diff options
author | James Lopez <james@jameslopez.es> | 2016-06-13 12:43:25 +0200 |
---|---|---|
committer | James Lopez <james@jameslopez.es> | 2016-06-13 12:43:25 +0200 |
commit | ad68bc63b5e7e8080e82d9febec05397c802d33d (patch) | |
tree | d9508d5bd96c8087d13b88051637bb442e877659 | |
parent | 41c06c311b9c5642f6056d0b34030980ebf507b3 (diff) | |
parent | cc322603945816ed957da7c0efa56e2ef1016569 (diff) | |
download | gitlab-ce-ad68bc63b5e7e8080e82d9febec05397c802d33d.tar.gz |
Merge branches 'feature/project-export' and 'feature/project-import' of gitlab.com:gitlab-org/gitlab-ce into feature/project-import
# Conflicts:
# app/models/project.rb
# db/schema.rb
# lib/gitlab/import_export/import_export_reader.rb
1830 files changed, 32805 insertions, 12102 deletions
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000000..2e88b7aa0a9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,3 @@ +We’re closing our issue tracker on GitHub so we can focus on the GitLab.com project and respond to issues more quickly. + +We encourage you to open an issue on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues). You can log into GitLab.com using your GitHub account. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..c3b04026440 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,3 @@ +Thank you for taking the time to contribute back to GitLab! + +Please open a merge request [on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests), we look forward to reviewing your contribution! You can log into GitLab.com using your GitHub account. diff --git a/.gitignore b/.gitignore index 8f861d76a37..ce6a363fe35 100644 --- a/.gitignore +++ b/.gitignore @@ -4,46 +4,46 @@ .bundle .chef .directory -.envrc -.gitlab_shell_secret +/.envrc +/.gitlab_shell_secret .idea -.rbenv-version +/.rbenv-version .rbx/ -.ruby-gemset -.ruby-version -.rvmrc +/.ruby-gemset +/.ruby-version +/.rvmrc .sass-cache/ -.secret -.vagrant -.byebug_history -Vagrantfile -backups/* -config/aws.yml -config/database.yml -config/gitlab.yml -config/gitlab_ci.yml -config/initializers/rack_attack.rb -config/initializers/smtp_settings.rb -config/initializers/relative_url.rb -config/resque.yml -config/unicorn.rb -config/secrets.yml -config/sidekiq.yml -coverage/* -db/*.sqlite3 -db/*.sqlite3-journal -db/data.yml -doc/code/* -dump.rdb -log/*.log* -nohup.out -public/assets/ -public/uploads.* -public/uploads/ -shared/artifacts/ -rails_best_practices_output.html +/.secret +/.vagrant +/.byebug_history +/Vagrantfile +/backups/* +/config/aws.yml +/config/database.yml +/config/gitlab.yml +/config/gitlab_ci.yml +/config/initializers/rack_attack.rb +/config/initializers/smtp_settings.rb +/config/initializers/relative_url.rb +/config/resque.yml +/config/unicorn.rb +/config/secrets.yml +/config/sidekiq.yml +/coverage/* +/db/*.sqlite3 +/db/*.sqlite3-journal +/db/data.yml +/doc/code/* +/dump.rdb +/log/*.log* +/nohup.out +/public/assets/ +/public/uploads.* +/public/uploads/ +/shared/artifacts/ +/rails_best_practices_output.html /tags -tmp/ -vendor/bundle/* -builds/* -shared/* +/tmp/* +/vendor/bundle/* +/builds/* +/shared/* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 85730e1b687..3dc48a89463 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,7 @@ image: "ruby:2.1" services: - mysql:latest - - redis:latest + - redis:alpine cache: key: "ruby21" @@ -13,229 +13,201 @@ variables: MYSQL_ALLOW_EMPTY_PASSWORD: "1" # retry tests only in CI environment RSPEC_RETRY_RETRY_COUNT: "3" + RAILS_ENV: "test" + SIMPLECOV: "true" + USE_DB: "true" + USE_BUNDLE_INSTALL: "true" before_script: - source ./scripts/prepare_build.sh - - ruby -v - - which ruby - - retry gem install bundler --no-ri --no-rdoc - cp config/gitlab.yml.example config/gitlab.yml - - touch log/application.log - - touch log/test.log - - retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - - RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate + - bundle --version + - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"' + - retry gem install knapsack + - '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' stages: +- prepare - test -- notifications +- post-test -spec:feature: - stage: test - script: - - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature - -spec:api: - stage: test - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api - -spec:models: - stage: test - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models - -spec:lib: - stage: test - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib - -spec:services: - stage: test - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services - -spec:other: - stage: test - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other - -spinach:project:half: - stage: test - script: - - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half - -spinach:project:rest: - stage: test - script: - - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest - -spinach:other: - stage: test - script: - - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other - -teaspoon: - stage: test - script: - - RAILS_ENV=test bundle exec teaspoon - -rubocop: - stage: test - script: - - bundle exec rubocop - -scss-lint: - stage: test - script: - - bundle exec rake scss_lint +# Prepare and merge knapsack tests -brakeman: - stage: test - script: - - bundle exec rake brakeman +.knapsack-state: &knapsack-state + services: [] + variables: + USE_DB: "false" + USE_BUNDLE_INSTALL: "false" + cache: + key: "knapsack" + paths: + - knapsack/ + artifacts: + paths: + - knapsack/ -flog: - stage: test +knapsack: + <<: *knapsack-state + stage: prepare script: - - bundle exec rake flog + - mkdir -p knapsack/ + - '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json' + - '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json' -flay: - stage: test +update-knapsack: + <<: *knapsack-state + stage: post-test script: - - bundle exec rake flay - -bundler:audit: - stage: test + - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json + - scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json + - rm -f knapsack/*_node_*.json only: - master - script: - - "bundle exec bundle-audit check --update --ignore OSVDB-115941" - -db-migrate-reset: - stage: test - script: - - RAILS_ENV=test bundle exec rake db:migrate:reset -# Ruby 2.2 jobs +# Execute all testing suites -spec:feature:ruby22: +.rspec-knapsack: &rspec-knapsack stage: test - image: ruby:2.2 - only: - - master script: - - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature - cache: - key: "ruby22" + - bundle exec rake assets:precompile 2>/dev/null + - JOB_NAME=( $CI_BUILD_NAME ) + - export CI_NODE_INDEX=${JOB_NAME[1]} + - export CI_NODE_TOTAL=${JOB_NAME[2]} + - export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json + - export KNAPSACK_GENERATE_REPORT=true + - cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH} + - knapsack rspec + artifacts: paths: - - vendor - -spec:api:ruby22: - stage: test - image: ruby:2.2 - only: - - master - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:api - cache: - key: "ruby22" - paths: - - vendor - -spec:models:ruby22: - stage: test - image: ruby:2.2 - only: - - master - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models - cache: - key: "ruby22" - paths: - - vendor - -spec:lib:ruby22: - stage: test - image: ruby:2.2 - only: - - master - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib - cache: - key: "ruby22" + - knapsack/ + +.spinach-knapsack: &spinach-knapsack + stage: test + script: + - bundle exec rake assets:precompile 2>/dev/null + - JOB_NAME=( $CI_BUILD_NAME ) + - export CI_NODE_INDEX=${JOB_NAME[1]} + - export CI_NODE_TOTAL=${JOB_NAME[2]} + - export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json + - export KNAPSACK_GENERATE_REPORT=true + - cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH} + - knapsack spinach "-r rerun" + # retry failed tests 3 times + - retry '[ ! -e tmp/spinach-rerun.txt ] || bin/spinach -r rerun $(cat tmp/spinach-rerun.txt)' + artifacts: paths: - - vendor - -spec:services:ruby22: - stage: test - image: ruby:2.2 + - knapsack/ + +rspec 0 20: *rspec-knapsack +rspec 1 20: *rspec-knapsack +rspec 2 20: *rspec-knapsack +rspec 3 20: *rspec-knapsack +rspec 4 20: *rspec-knapsack +rspec 5 20: *rspec-knapsack +rspec 6 20: *rspec-knapsack +rspec 7 20: *rspec-knapsack +rspec 8 20: *rspec-knapsack +rspec 9 20: *rspec-knapsack +rspec 10 20: *rspec-knapsack +rspec 11 20: *rspec-knapsack +rspec 12 20: *rspec-knapsack +rspec 13 20: *rspec-knapsack +rspec 14 20: *rspec-knapsack +rspec 15 20: *rspec-knapsack +rspec 16 20: *rspec-knapsack +rspec 17 20: *rspec-knapsack +rspec 18 20: *rspec-knapsack +rspec 19 20: *rspec-knapsack + +spinach 0 10: *spinach-knapsack +spinach 1 10: *spinach-knapsack +spinach 2 10: *spinach-knapsack +spinach 3 10: *spinach-knapsack +spinach 4 10: *spinach-knapsack +spinach 5 10: *spinach-knapsack +spinach 6 10: *spinach-knapsack +spinach 7 10: *spinach-knapsack +spinach 8 10: *spinach-knapsack +spinach 9 10: *spinach-knapsack + +# Execute all testing suites against Ruby 2.2 + +.ruby-22: &ruby-22 + image: "ruby:2.2" only: - - master - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services - cache: - key: "ruby22" - paths: - - vendor - -spec:other:ruby22: - stage: test - image: ruby:2.2 - only: - - master - script: - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other + - master cache: key: "ruby22" paths: - vendor -spinach:project:half:ruby22: - stage: test - image: ruby:2.2 - only: - - master - script: - - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half - cache: - key: "ruby22" - paths: - - vendor +.rspec-knapsack-ruby22: &rspec-knapsack-ruby22 + <<: *rspec-knapsack + <<: *ruby-22 + +.spinach-knapsack-ruby22: &spinach-knapsack-ruby22 + <<: *spinach-knapsack + <<: *ruby-22 + +rspec 0 20 ruby22: *rspec-knapsack-ruby22 +rspec 1 20 ruby22: *rspec-knapsack-ruby22 +rspec 2 20 ruby22: *rspec-knapsack-ruby22 +rspec 3 20 ruby22: *rspec-knapsack-ruby22 +rspec 4 20 ruby22: *rspec-knapsack-ruby22 +rspec 5 20 ruby22: *rspec-knapsack-ruby22 +rspec 6 20 ruby22: *rspec-knapsack-ruby22 +rspec 7 20 ruby22: *rspec-knapsack-ruby22 +rspec 8 20 ruby22: *rspec-knapsack-ruby22 +rspec 9 20 ruby22: *rspec-knapsack-ruby22 +rspec 10 20 ruby22: *rspec-knapsack-ruby22 +rspec 11 20 ruby22: *rspec-knapsack-ruby22 +rspec 12 20 ruby22: *rspec-knapsack-ruby22 +rspec 13 20 ruby22: *rspec-knapsack-ruby22 +rspec 14 20 ruby22: *rspec-knapsack-ruby22 +rspec 15 20 ruby22: *rspec-knapsack-ruby22 +rspec 16 20 ruby22: *rspec-knapsack-ruby22 +rspec 17 20 ruby22: *rspec-knapsack-ruby22 +rspec 18 20 ruby22: *rspec-knapsack-ruby22 +rspec 19 20 ruby22: *rspec-knapsack-ruby22 + +spinach 0 10 ruby22: *spinach-knapsack-ruby22 +spinach 1 10 ruby22: *spinach-knapsack-ruby22 +spinach 2 10 ruby22: *spinach-knapsack-ruby22 +spinach 3 10 ruby22: *spinach-knapsack-ruby22 +spinach 4 10 ruby22: *spinach-knapsack-ruby22 +spinach 5 10 ruby22: *spinach-knapsack-ruby22 +spinach 6 10 ruby22: *spinach-knapsack-ruby22 +spinach 7 10 ruby22: *spinach-knapsack-ruby22 +spinach 8 10 ruby22: *spinach-knapsack-ruby22 +spinach 9 10 ruby22: *spinach-knapsack-ruby22 + +# Other generic tests + +.exec: &exec + stage: test + script: + - bundle exec $CI_BUILD_NAME + +teaspoon: *exec +rubocop: *exec +rake scss_lint: *exec +rake brakeman: *exec +rake flog: *exec +rake flay: *exec +rake db:migrate:reset: *exec +license_finder: *exec -spinach:project:rest:ruby22: +bundler:audit: stage: test - image: ruby:2.2 only: - - master + - master script: - - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest - cache: - key: "ruby22" - paths: - - vendor + - "bundle exec bundle-audit check --update --ignore OSVDB-115941" -spinach:other:ruby22: - stage: test - image: ruby:2.2 - only: - - master - script: - - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null - - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:other - cache: - key: "ruby22" - paths: - - vendor +# Notify slack in the end notify:slack: - stage: notifications + stage: post-test script: - ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>" when: on_failure diff --git a/.rubocop.yml b/.rubocop.yml index 9f179efa3ce..c637f5e12f5 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,5 @@ +require: rubocop-rspec + AllCops: TargetRubyVersion: 2.1 # Cop names are not displayed in offense messages by default. Change behavior @@ -11,7 +13,8 @@ AllCops: # Exclude some GitLab files Exclude: - 'vendor/**/*' - - 'db/**/*' + - 'db/*' + - 'db/fixtures/**/*' - 'tmp/**/*' - 'bin/**/*' - 'lib/backup/**/*' @@ -21,6 +24,7 @@ AllCops: - 'lib/email_validator.rb' - 'lib/gitlab/upgrader.rb' - 'lib/gitlab/seeder.rb' + - 'generator_templates/**/*' ##################### Style ################################## @@ -56,7 +60,7 @@ Style/AndOr: # Use `Array#join` instead of `Array#*`. Style/ArrayJoin: - Enabled: false + Enabled: true # Use only ascii symbols in comments. Style/AsciiComments: @@ -68,7 +72,7 @@ Style/AsciiIdentifiers: # Checks for uses of Module#attr. Style/Attr: - Enabled: false + Enabled: true # Avoid the use of BEGIN blocks. Style/BeginBlock: @@ -80,7 +84,7 @@ Style/BarePercentLiterals: # Do not use block comments. Style/BlockComments: - Enabled: false + Enabled: true # Put end statement of multiline block on its own line. Style/BlockEndNewline: @@ -121,7 +125,7 @@ Style/ClassCheck: # Use self when defining module/class methods. Style/ClassMethods: - Enabled: false + Enabled: true # Avoid the use of class variables. Style/ClassVars: @@ -151,7 +155,7 @@ Style/ConstantName: # Use def with parentheses when there are arguments. Style/DefWithParentheses: - Enabled: false + Enabled: true # Checks for use of deprecated Hash methods. Style/DeprecatedHashMethods: @@ -191,7 +195,7 @@ Style/EmptyLines: # Keep blank lines around access modifiers. Style/EmptyLinesAroundAccessModifier: - Enabled: false + Enabled: true # Keeps track of empty lines around block bodies. Style/EmptyLinesAroundBlockBody: @@ -215,15 +219,15 @@ Style/EmptyLiteral: # Avoid the use of END blocks. Style/EndBlock: - Enabled: false + Enabled: true # Use Unix-style line endings. Style/EndOfLine: - Enabled: false + Enabled: true # Favor the use of Fixnum#even? && Fixnum#odd? Style/EvenOdd: - Enabled: false + Enabled: true # Do not use unnecessary spacing. Style/ExtraSpacing: @@ -231,15 +235,20 @@ Style/ExtraSpacing: # Use snake_case for source file names. Style/FileName: - Enabled: false + Enabled: true + +# Checks for a line break before the first parameter in a multi-line method +# parameter definition. +Style/FirstMethodParameterLineBreak: + Enabled: true # Checks for flip flops. Style/FlipFlop: - Enabled: false + Enabled: true # Checks use of for or each in multiline loops. Style/For: - Enabled: false + Enabled: true # Enforce the use of Kernel#sprintf, Kernel#format or String#%. Style/FormatString: @@ -247,7 +256,7 @@ Style/FormatString: # Do not introduce global variables. Style/GlobalVars: - Enabled: false + Enabled: true # Check for conditionals that can be replaced with guard clauses. Style/GuardClause: @@ -268,7 +277,7 @@ Style/IfUnlessModifier: # Do not use if x; .... Use the ternary operator instead. Style/IfWithSemicolon: - Enabled: false + Enabled: true # Checks that conditional statements do not have an identical line at the # end of each branch, which can validly be moved out of the conditional. @@ -276,9 +285,9 @@ Style/IdenticalConditionalBranches: Enabled: false # Checks the indentation of the first line of the right-hand-side of a -# multi-line assignment. +# multi-line assignment. Style/IndentAssignment: - Enabled: false + Enabled: true # Keep indentation straight. Style/IndentationConsistency: @@ -298,7 +307,7 @@ Style/IndentHash: # Use Kernel#loop for infinite loops. Style/InfiniteLoop: - Enabled: false + Enabled: true # Use the new lambda literal syntax for single-line blocks. Style/Lambda: @@ -306,11 +315,11 @@ Style/Lambda: # Use lambda.call(...) instead of lambda.(...). Style/LambdaCall: - Enabled: false + Enabled: true # Comments should start with a space. Style/LeadingCommentSpace: - Enabled: false + Enabled: true # Use \ instead of + or << to concatenate two string literals at line end. Style/LineEndConcatenation: @@ -322,16 +331,22 @@ Style/MethodCallParentheses: # Checks if the method definitions have or don't have parentheses. Style/MethodDefParentheses: - Enabled: false + Enabled: true # Use the configured style when naming methods. Style/MethodName: - Enabled: false + Enabled: true # Checks for usage of `extend self` in modules. Style/ModuleFunction: Enabled: false +# Checks that the closing brace in an array literal is either on the same line +# as the last array element, or a new line. +Style/MultilineArrayBraceLayout: + Enabled: false + EnforcedStyle: symmetrical + # Avoid multi-line chains of blocks. Style/MultilineBlockChain: Enabled: false @@ -340,15 +355,32 @@ Style/MultilineBlockChain: Style/MultilineBlockLayout: Enabled: true +# Checks that the closing brace in a hash literal is either on the same line as +# the last hash element, or a new line. +Style/MultilineHashBraceLayout: + Enabled: false + EnforcedStyle: symmetrical + # Do not use then for multi-line if/unless. Style/MultilineIfThen: + Enabled: true + +# Checks that the closing brace in a method call is either on the same line as +# the last method argument, or a new line. +Style/MultilineMethodCallBraceLayout: Enabled: false + EnforcedStyle: symmetrical # Checks indentation of method calls with the dot operator that span more than # one line. Style/MultilineMethodCallIndentation: Enabled: false +# Checks that the closing brace in a method definition is symmetrical with +# respect to the opening brace and the method parameters. +Style/MultilineMethodDefinitionBraceLayout: + Enabled: false + # Checks indentation of binary operations that span more than one line. Style/MultilineOperationIndentation: Enabled: false @@ -363,7 +395,7 @@ Style/MutableConstant: # Favor unless over if for negative conditions (or control flow or). Style/NegatedIf: - Enabled: false + Enabled: true # Favor until over while for negative conditions. Style/NegatedWhile: @@ -371,7 +403,7 @@ Style/NegatedWhile: # Avoid using nested modifiers. Style/NestedModifier: - Enabled: false + Enabled: true # Parenthesize method calls which are nested inside the argument list of # another parenthesized method call. @@ -408,7 +440,7 @@ Style/OneLineConditional: # When defining binary operators, name the argument other. Style/OpMethod: - Enabled: false + Enabled: true # Check for simple usages of parallel assignment. It will only warn when # the number of variables matches on both sides of the assignment. @@ -455,10 +487,9 @@ Style/RedundantException: Style/RedundantFreeze: Enabled: false -# TODO: Enable RedundantParentheses Cop. # Checks for parentheses that seem not to serve any purpose. Style/RedundantParentheses: - Enabled: false + Enabled: true # Don't use return where it's not required. Style/RedundantReturn: @@ -484,11 +515,12 @@ Style/SelfAssignment: # Don't use semicolons to terminate expressions. Style/Semicolon: - Enabled: false + Enabled: true # Checks for proper usage of fail and raise. Style/SignalException: - Enabled: false + EnforcedStyle: only_raise + Enabled: true # Enforces the names of some block params. Style/SingleLineBlockParams: @@ -509,29 +541,28 @@ Style/SpaceAfterComma: # Do not put a space between a method name and the opening parenthesis in a # method definition. Style/SpaceAfterMethodName: - Enabled: false + Enabled: true # Tracks redundant space after the ! operator. Style/SpaceAfterNot: - Enabled: false + Enabled: true # Use spaces after semicolons. Style/SpaceAfterSemicolon: - Enabled: false + Enabled: true # Checks that the equals signs in parameter default assignments have or don't # have surrounding space depending on configuration. Style/SpaceAroundEqualsInParameterDefault: Enabled: false -# TODO: Enable SpaceAroundKeyword Cop. # Use a space around keywords if appropriate. Style/SpaceAroundKeyword: - Enabled: false + Enabled: true # Use a single space around operators. Style/SpaceAroundOperators: - Enabled: false + Enabled: true # Checks that the left block brace has or doesn't have space before it. Style/SpaceBeforeBlockBraces: @@ -539,11 +570,11 @@ Style/SpaceBeforeBlockBraces: # No spaces before commas. Style/SpaceBeforeComma: - Enabled: false + Enabled: true # Checks for missing space between code and a comment on the same line. Style/SpaceBeforeComment: - Enabled: false + Enabled: true # Checks that exactly one space is used between a method name and the first # argument for method calls without parentheses. @@ -552,7 +583,7 @@ Style/SpaceBeforeFirstArg: # No spaces before semicolons. Style/SpaceBeforeSemicolon: - Enabled: false + Enabled: true # Checks that block braces have or don't have surrounding space. # For blocks taking parameters, checks that the left brace has or doesn't @@ -574,11 +605,12 @@ Style/SpaceInsideParens: # No spaces inside range literals. Style/SpaceInsideRangeLiteral: - Enabled: false + Enabled: true # Checks for padding/surrounding spaces inside string interpolation. Style/SpaceInsideStringInterpolation: - Enabled: false + EnforcedStyle: no_space + Enabled: true # Avoid Perl-style global variables. Style/SpecialGlobalVars: @@ -586,7 +618,8 @@ Style/SpecialGlobalVars: # Check for the usage of parentheses around stabby lambda arguments. Style/StabbyLambdaParentheses: - Enabled: false + EnforcedStyle: require_parentheses + Enabled: true # Checks if uses of quotes match the configured preference. Style/StringLiterals: @@ -599,7 +632,9 @@ Style/StringLiteralsInInterpolation: # Checks if configured preferred methods are used over non-preferred. Style/StringMethods: - Enabled: false + PreferredMethods: + intern: to_sym + Enabled: true # Use %i or %I for arrays of symbols. Style/SymbolArray: @@ -657,23 +692,24 @@ Style/UnneededPercentQ: # Don't interpolate global, instance and class variables directly in strings. Style/VariableInterpolation: - Enabled: false + Enabled: true # Use the configured style when naming variables. Style/VariableName: - Enabled: false + EnforcedStyle: snake_case + Enabled: true # Use when x then ... for one-line cases. Style/WhenThen: - Enabled: false + Enabled: true # Checks for redundant do after while or until. Style/WhileUntilDo: - Enabled: false + Enabled: true # Favor modifier while/until usage when you have a single-line body. Style/WhileUntilModifier: - Enabled: false + Enabled: true # Use %w or %W for arrays of words. Style/WordArray: @@ -736,7 +772,7 @@ Metrics/PerceivedComplexity: # Checks for ambiguous operators in the first argument of a method invocation # without parentheses. Lint/AmbiguousOperator: - Enabled: false + Enabled: true # Checks for ambiguous regexp literals in the first argument of a method # invocation without parentheses. @@ -749,28 +785,28 @@ Lint/AssignmentInCondition: # Align block ends correctly. Lint/BlockAlignment: - Enabled: false + Enabled: true # Default values in optional keyword arguments and optional ordinal arguments # should not refer back to the name of the argument. Lint/CircularArgumentReference: - Enabled: false + Enabled: true # Checks for condition placed in a confusing position relative to the keyword. Lint/ConditionPosition: - Enabled: false + Enabled: true # Check for debugger calls. Lint/Debugger: - Enabled: false + Enabled: true # Align ends corresponding to defs correctly. Lint/DefEndAlignment: - Enabled: false + Enabled: true # Check for deprecated class method calls. Lint/DeprecatedClassMethods: - Enabled: false + Enabled: true # Check for duplicate method definitions. Lint/DuplicateMethods: @@ -782,15 +818,15 @@ Lint/DuplicatedKey: # Check for immutable argument given to each_with_object. Lint/EachWithObjectArgument: - Enabled: false + Enabled: true # Check for odd code arrangement in an else block. Lint/ElseLayout: - Enabled: false + Enabled: true # Checks for empty ensure block. Lint/EmptyEnsure: - Enabled: false + Enabled: true # Checks for empty string interpolation. Lint/EmptyInterpolation: @@ -798,37 +834,36 @@ Lint/EmptyInterpolation: # Align ends correctly. Lint/EndAlignment: - Enabled: false + Enabled: true # END blocks should not be placed inside method definitions. Lint/EndInMethod: - Enabled: false + Enabled: true # Do not use return in an ensure block. Lint/EnsureReturn: - Enabled: false + Enabled: true # The use of eval represents a serious security risk. Lint/Eval: - Enabled: false + Enabled: true # Catches floating-point literals too large or small for Ruby to represent. Lint/FloatOutOfRange: - Enabled: false + Enabled: true # The number of parameters to format/sprint must match the fields. Lint/FormatParameterMismatch: - Enabled: false + Enabled: true # Don't suppress exception. Lint/HandleExceptions: Enabled: false -# TODO: Enable ImplicitStringConcatenation Cop. # Checks for adjacent string literals on the same line, which could better be # represented as a single string literal. Lint/ImplicitStringConcatenation: - Enabled: false + Enabled: true # TODO: Enable IneffectiveAccessModifier Cop. # Checks for attempts to use `private` or `protected` to set the visibility @@ -839,15 +874,15 @@ Lint/IneffectiveAccessModifier: # Checks for invalid character literals with a non-escaped whitespace # character. Lint/InvalidCharacterLiteral: - Enabled: false + Enabled: true # Checks of literals used in conditions. Lint/LiteralInCondition: - Enabled: false + Enabled: true # Checks for literals used in interpolation. Lint/LiteralInInterpolation: - Enabled: false + Enabled: true # Use Kernel#loop with break rather than begin/end/until or begin/end/while # for post-loop tests. @@ -856,11 +891,11 @@ Lint/Loop: # Do not use nested method definitions. Lint/NestedMethodDefinition: - Enabled: false + Enabled: true # Do not omit the accumulator when calling `next` in a `reduce`/`inject` block. Lint/NextWithoutAccumulator: - Enabled: false + Enabled: true # Checks for method calls with a space before the opening parenthesis. Lint/ParenthesesAsGroupedExpression: @@ -869,11 +904,11 @@ Lint/ParenthesesAsGroupedExpression: # Checks for `rand(1)` calls. Such calls always return `0` and most likely # a mistake. Lint/RandOne: - Enabled: false + Enabled: true # Use parentheses in the method call to avoid confusion about precedence. Lint/RequireParentheses: - Enabled: false + Enabled: true # Avoid rescuing the Exception class. Lint/RescueException: @@ -908,7 +943,7 @@ Lint/UnusedMethodArgument: # Unreachable code. Lint/UnreachableCode: - Enabled: false + Enabled: true # Checks for useless access modifiers. Lint/UselessAccessModifier: @@ -920,33 +955,31 @@ Lint/UselessAssignment: # Checks for comparison of something with itself. Lint/UselessComparison: - Enabled: false + Enabled: true # Checks for useless `else` in `begin..end` without `rescue`. Lint/UselessElseWithoutRescue: - Enabled: false + Enabled: true # Checks for useless setter call to a local variable. Lint/UselessSetterCall: - Enabled: false + Enabled: true # Possible use of operator/literal/variable in void context. Lint/Void: - Enabled: false + Enabled: true ##################### Performance ############################ -# TODO: Enable Casecmp Cop. # Use `casecmp` rather than `downcase ==`. Performance/Casecmp: - Enabled: false + Enabled: true -# TODO: Enable DoubleStartEndWith Cop. # Use `str.{start,end}_with?(x, ..., y, ...)` instead of # `str.{start,end}_with?(x, ...) || str.{start,end}_with?(y, ...)`. Performance/DoubleStartEndWith: - Enabled: false + Enabled: true # TODO: Enable EndWith Cop. # Use `end_with?` instead of a regex match anchored to the end of a string. @@ -957,10 +990,9 @@ Performance/EndWith: Performance/LstripRstrip: Enabled: true -# TODO: Enable RangeInclude Cop. # Use `Range#cover?` instead of `Range#include?`. Performance/RangeInclude: - Enabled: false + Enabled: true # TODO: Enable RedundantBlockCall Cop. # Use `yield` instead of `block.call`. @@ -980,26 +1012,24 @@ Performance/RedundantMerge: MaxKeyValuePairs: 2 Enabled: false -# TODO: Enable RedundantSortBy Cop. # Use `sort` instead of `sort_by { |x| x }`. Performance/RedundantSortBy: - Enabled: false + Enabled: true -# TODO: Enable StartWith Cop. # Use `start_with?` instead of a regex match anchored to the beginning of a # string. Performance/StartWith: - Enabled: false + Enabled: true + # Use `tr` instead of `gsub` when you are replacing the same number of # characters. Use `delete` instead of `gsub` when you are deleting # characters. Performance/StringReplacement: - Enabled: false + Enabled: true -# TODO: Enable TimesMap Cop. # Checks for `.times.map` calls. Performance/TimesMap: - Enabled: false + Enabled: true ##################### Rails ################################## @@ -1024,11 +1054,11 @@ Rails/Delegate: # Prefer `find_by` over `where.first`. Rails/FindBy: - Enabled: false + Enabled: true # Prefer `all.find_each` over `all.find`. Rails/FindEach: - Enabled: false + Enabled: true # Prefer has_many :through to has_and_belongs_to_many. Rails/HasAndBelongsToMany: @@ -1040,7 +1070,7 @@ Rails/Output: # Checks for incorrect grammar when using methods like `3.day.ago`. Rails/PluralizationGrammar: - Enabled: false + Enabled: true # Checks for `read_attribute(:attr)` and `write_attribute(:attr, val)`. Rails/ReadWriteAttribute: @@ -1048,7 +1078,7 @@ Rails/ReadWriteAttribute: # Checks the arguments of ActiveRecord scopes. Rails/ScopeArgs: - Enabled: false + Enabled: true # Checks the correct usage of time zone aware methods. # http://danilenko.org/2012/7/6/rails_timezones @@ -1058,3 +1088,68 @@ Rails/TimeZone: # Use validates :attribute, hash of validations. Rails/Validation: Enabled: false + +Rails/UniqBeforePluck: + Enabled: false + +##################### RSpec ################################## + +# Check that instances are not being stubbed globally. +RSpec/AnyInstance: + Enabled: false + +# Check that the first argument to the top level describe is the tested class or +# module. +RSpec/DescribeClass: + Enabled: false + +# Use `described_class` for tested class / module. +RSpec/DescribeMethod: + Enabled: false + +# Checks that the second argument to top level describe is the tested method +# name. +RSpec/DescribedClass: + Enabled: false + +# Checks for long example. +RSpec/ExampleLength: + Enabled: false + Max: 5 + +# Do not use should when describing your tests. +RSpec/ExampleWording: + Enabled: false + CustomTransform: + be: is + have: has + not: does not + IgnoredWords: [] + +# Checks the file and folder naming of the spec file. +RSpec/FilePath: + Enabled: false + CustomTransform: + RuboCop: rubocop + RSpec: rspec + +# Checks if there are focused specs. +RSpec/Focus: + Enabled: true + +# Checks for the usage of instance variables. +RSpec/InstanceVariable: + Enabled: false + +# Checks for multiple top-level describes. +RSpec/MultipleDescribes: + Enabled: false + +# Enforces the usage of the same method on all negative message expectations. +RSpec/NotToNot: + EnforcedStyle: not_to + Enabled: true + +# Prefer using verifying doubles over normal doubles. +RSpec/VerifiedDoubles: + Enabled: false diff --git a/app/views/projects/notes/_commit_discussion.html.haml b/.vagrant_enabled index e69de29bb2d..e69de29bb2d 100644 --- a/app/views/projects/notes/_commit_discussion.html.haml +++ b/.vagrant_enabled diff --git a/CHANGELOG b/CHANGELOG index 822fa4be565..364690286e1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,47 +1,229 @@ Please view this file on the master branch, on stable branches it's out of date. -v 8.8.0 (unreleased) +v 8.9.0 (unreleased) + - Fix Error 500 when using closes_issues API with an external issue tracker + - Bulk assign/unassign labels to issues. + - Ability to prioritize labels !4009 / !3205 (Thijs Wouters) + - Fix endless redirections when accessing user OAuth applications when they are disabled + - Allow enabling wiki page events from Webhook management UI + - Bump rouge to 1.11.0 + - Fix issue with arrow keys not working in search autocomplete dropdown + - Make EmailsOnPushWorker use Sidekiq mailers queue + - Fix wiki page events' webhook to point to the wiki repository + - Fix issue todo not remove when leave project !4150 (Long Nguyen) + - Allow customisable text on the 'nearly there' page after a user signs up + - Bump recaptcha gem to 3.0.0 to remove deprecated stoken support + - Fix SVG sanitizer to allow more elements + - Allow forking projects with restricted visibility level + - Added descriptions to notification settings dropdown + - Improve note validation to prevent errors when creating invalid note via API + - Reduce number of fog gem dependencies + - Remove project notification settings associated with deleted projects + - Fix 404 page when viewing TODOs that contain milestones or labels in different projects + - Redesign navigation for project pages + - Fix groups API to list only user's accessible projects + - Redesign account and email confirmation emails + - `git clone https://host/namespace/project` now works, in addition to using the `.git` suffix + - Bump nokogiri to 1.6.8 + - Use gitlab-shell v3.0.0 + - Upgrade to jQuery 2 + - Use Knapsack to evenly distribute tests across multiple nodes + - Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged + - Don't allow MRs to be merged when commits were added since the last review / page load + - Add DB index on users.state + - Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database + - Changed the Slack build message to use the singular duration if necessary (Aran Koning) + - Links from a wiki page to other wiki pages should be rewritten as expected + - Add option to project to only allow merge requests to be merged if the build succeeds (Rui Santos) + - Fix issues filter when ordering by milestone + - Todos will display target state if issuable target is 'Closed' or 'Merged' + - Fix bug when sorting issues by milestone due date and filtering by two or more labels + - Add support for using Yubikeys (U2F) for two-factor authentication + - Link to blank group icon doesn't throw a 404 anymore + - Remove 'main language' feature + - Pipelines can be canceled only when there are running builds + - Use downcased path to container repository as this is expected path by Docker + - Projects pending deletion will render a 404 page + - Measure queue duration between gitlab-workhorse and Rails + - Make Omniauth providers specs to not modify global configuration + - Make authentication service for Container Registry to be compatible with < Docker 1.11 + - Add Application Setting to configure Container Registry token expire delay (default 5min) + - Cache assigned issue and merge request counts in sidebar nav + - Use Knapsack only in CI environment + - Cache project build count in sidebar nav + - Add milestone expire date to the right sidebar + - Fix markdown_spec to use before instead of before(:all) to properly cleanup database after testing + - Reduce number of queries needed to render issue labels in the sidebar + - Improve error handling importing projects + - Remove duplicated notification settings + - Put project Files and Commits tabs under Code tab + - Replace Colorize with Rainbow for coloring console output in Rake tasks. + - Add workhorse controller and API helpers + - An indicator is now displayed at the top of the comment field for confidential issues. + - RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented + - Improve issuables APIs performance when accessing notes !4471 + - External links now open in a new tab + - Markdown editor now correctly resets the input value on edit cancellation !4175 + - Toggling a task list item in a issue/mr description does not creates a Todo for mentions + - Improved UX of date pickers on issue & milestone forms + - Cache on the database if a project has an active external issue tracker. + - Put project Labels and Milestones pages links under Issues and Merge Requests tabs as subnav + +v 8.8.5 (unreleased) + - Ensure branch cleanup regardless of whether the GitHub import process succeeds + - Fix todos page throwing errors when you have a project pending deletion + - Reduce number of SQL queries when rendering user references + - Import GitHub repositories respecting the API rate limit + - Fix importer for GitHub comments on diff + - Disable Webhooks before proceeding with the GitHub import + - Fix incremental trace upload API when using multi-byte UTF-8 chars in trace + +v 8.8.4 + - Fix LDAP-based login for users with 2FA enabled. !4493 + +v 8.8.3 + - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 + - Fixed JS error when trying to remove discussion form. !4303 + - Fixed issue with button color when no CI enabled. !4287 + - Fixed potential issue with 2 CI status polling events happening. !3869 + - Improve design of Pipeline view. !4230 + - Fix gitlab importer failing to import new projects due to missing credentials. !4301 + - Fix import URL migration not rescuing with the correct Error. !4321 + - Fix health check access token changing due to old application settings being used. !4332 + - Make authentication service for Container Registry to be compatible with Docker versions before 1.11. !4363 + - Add Application Setting to configure Container Registry token expire delay (default 5 min). !4364 + - Pass the "Remember me" value to the 2FA token form. !4369 + - Fix incorrect links on pipeline page when merge request created from fork. !4376 + - Use downcased path to container repository as this is expected path by Docker. !4420 + - Fix wiki project clone address error (chujinjin). !4429 + - Fix serious performance bug with rendering Markdown with InlineDiffFilter. !4392 + - Fix missing number on generated ordered list element. !4437 + - Prevent disclosure of notes on confidential issues in search results. + +v 8.8.2 + - Added remove due date button. !4209 + - Fix Error 500 when accessing application settings due to nil disabled OAuth sign-in sources. !4242 + - Fix Error 500 in CI charts by gracefully handling commits with no durations. !4245 + - Fix table UI on CI builds page. !4249 + - Fix backups if registry is disabled. !4263 + - Fixed issue with merge button color. !4211 + - Fixed issue with enter key selecting wrong option in dropdown. !4210 + - When creating a .gitignore file a dropdown with templates will be provided. !4075 + - Fix concurrent request when updating build log in browser. !4183 + +v 8.8.1 + - Add documentation for the "Health Check" feature + - Allow anonymous users to access a public project's pipelines !4233 + - Fix MySQL compatibility in zero downtime migrations helpers + - Fix the CI login to Container Registry (the gitlab-ci-token user) + +v 8.8.0 + - Implement GFM references for milestones (Alejandro RodrÃguez) + - Snippets tab under user profile. !4001 (Long Nguyen) + - Fix error when using link to uploads in global snippets + - Fix Error 500 when attempting to retrieve project license when HEAD points to non-existent ref - Assign labels and milestone to target project when moving issue. !3934 (Long Nguyen) + - Use a case-insensitive comparison in sanitizing URI schemes + - Toggle sign-up confirmation emails in application settings + - Make it possible to prevent tagged runner from picking untagged jobs + - Added `InlineDiffFilter` to the markdown parser. (Adam Butler) + - Added inline diff styling for `change_title` system notes. (Adam Butler) - Project#open_branches has been cleaned up and no longer loads entire records into memory. - Escape HTML in commit titles in system note messages + - Improve design of Pipeline View + - Fix scope used when accessing container registry + - Fix creation of Ci::Commit object which can lead to pending, failed in some scenarios - Improve multiple branch push performance by memoizing permission checking - Log to application.log when an admin starts and stops impersonating a user + - Changing the confidentiality of an issue now creates a new system note (Alex Moore-Niemi) - Updated gitlab_git to 10.1.0 - GitAccess#protected_tag? no longer loads all tags just to check if a single one exists - Reduce delay in destroying a project from 1-minute to immediately - Make build status canceled if any of the jobs was canceled and none failed - Upgrade Sidekiq to 4.1.2 + - Added /health_check endpoint for checking service status + - Make 'upcoming' filter for milestones work better across projects - Sanitize repo paths in new project error message - Bump mail_room to 0.7.0 to fix stuck IDLE connections - Remove future dates from contribution calendar graph. - Support e-mail notifications for comments on project snippets + - Fix API leak of notes of unauthorized issues, snippets and merge requests - Use ActionDispatch Remote IP for Akismet checking - Fix error when visiting commit builds page before build was updated - Add 'l' shortcut to open Label dropdown on issuables and 'i' to create new issue on a project - Update SVG sanitizer to conform to SVG 1.1 + - Speed up push emails with multiple recipients by only generating the email once - Updated search UI + - Added authentication service for Container Registry - Display informative message when new milestone is created + - Sanitize milestones and labels titles + - Support multi-line tag messages. !3833 (Calin Seciu) + - Force users to reset their password after an admin changes it - Allow "NEWS" and "CHANGES" as alternative names for CHANGELOG. !3768 (Connor Shea) - Added button to toggle whitespaces changes on diff view - Backport GitHub Enterprise import support from EE - Create tags using Rugged for performance reasons. !3745 + - Allow guests to set notification level in projects + - API: Expose Issue#user_notes_count. !3126 (Anton Popov) + - Don't show forks button when user can't view forks + - Fix atom feed links and rendering - Files over 5MB can only be viewed in their raw form, files over 1MB without highlighting !3718 - Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes) + - Add eager load paths to help prevent dependency load issues in Sidekiq workers. !3724 - Added multiple colors for labels in dropdowns when dups happen. + - Show commits in the same order as `git log` - Improve description for the Two-factor Authentication sign-in screen. (Connor Shea) - API support for the 'since' and 'until' operators on commit requests (Paco Guzman) - Fix Gravatar hint in user profile when Gravatar is disabled. !3988 (Artem Sidorenko) - Expire repository exists? and has_visible_content? caches after a push if necessary - - Fix unintentional filtering bug in issues sorted by milestone due (Takuya Noguchi) + - Fix unintentional filtering bug in Issue/MR sorted by milestone due (Takuya Noguchi) + - Fix adding a todo for private group members (Ahmad Sherif) + - Bump ace-rails-ap gem version from 2.0.1 to 4.0.2 which upgrades Ace Editor from 1.1.2 to 1.2.3 + - Total method execution timings are no longer tracked + - Allow Admins to remove the Login with buttons for OAuth services and still be able to import !4034. (Andrei Gliga) + - Add API endpoints for un/subscribing from/to a label. !4051 (Ahmad Sherif) + - Hide left sidebar on phone screens to give more space for content + - Redesign navigation for profile and group pages + - Add counter metrics for rails cache + - Import pull requests from GitHub where the source or target branches were removed + - All Grape API helpers are now instrumented + - Improve Issue formatting for the Slack Service (Jeroen van Baarsen) + - Fixed advice on invalid permissions on upload path !2948 (Ludovic Perrine) + - Allows MR authors to have the source branch removed when merging the MR. !2801 (Jeroen Jacobs) + - When creating a .gitignore file a dropdown with templates will be provided + - Shows the issue/MR list search/filter form and corrects the mobile styling for guest users. #17562 + +v 8.7.7 + - Fix import by `Any Git URL` broken if the URL contains a space + +v 8.7.6 + - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) + - Fix import from GitLab.com to a private instance failure. !4181 + - Fix external imports not finding the import data. !4106 + - Fix notification delay when changing status of an issue + - Bump Workhorse to 0.7.5 so it can serve raw diffs + +v 8.7.5 + - Fix relative links in wiki pages. !4050 + - Fix always showing build notification message when switching between merge requests !4086 + - Fix an issue when filtering merge requests with more than one label. !3886 + - Fix short note for the default scope on build page (Takuya Noguchi) v 8.7.4 - - Fix always showing build notification message when switching between merge requests + - Links for Redmine issue references are generated correctly again !4048 (Benedikt Huss) + - Fix setting trusted proxies !3970 + - Fix BitBucket importer bug when throwing exceptions !3941 + - Use sign out path only if not empty !3989 + - Running rake gitlab:db:drop_tables now drops tables with cascade !4020 + - Running rake gitlab:db:drop_tables uses "IF EXISTS" as a precaution !4100 + - Use a case-insensitive comparison in sanitizing URI schemes v 8.7.3 - Emails, Gitlab::Email::Message, Gitlab::Diff, and Premailer::Adapter::Nokogiri are now instrumented - Merge request widget displays TeamCity build state and code coverage correctly again. - Fix the line code when importing PR review comments from GitHub. !4010 - Wikis are now initialized on legacy projects when checking repositories + - Remove animate.css in favor of a smaller subset of animations. !3937 (Connor Shea) v 8.7.2 - The "New Branch" button is now loaded asynchronously @@ -850,7 +1032,7 @@ v 8.1.3 - Use issue editor as cross reference comment author when issue is edited with a new mention - Add Facebook authentication -v 8.1.2 +v 8.1.1 - Fix cloning Wiki repositories via HTTP (Stan Hu) - Add migration to remove satellites directory - Fix specific runners visibility @@ -1475,20 +1657,17 @@ v 7.10.0 - Fix stuck Merge Request merging events from old installations (Ben Bodenmiller) - Fix merge request comments on files with multiple commits - Fix Resource Owner Password Authentication Flow - -v 7.9.4 - - Security: Fix project import URL regex to prevent arbitary local repos from being imported - - Fixed issue where only 25 commits would load in file listings - - Fix LDAP identities after config update - -v 7.9.3 - - Contains no changes - Add icons to Add dropdown items. - Allow admin to create public deploy keys that are accessible to any project. - Warn when gitlab-shell version doesn't match requirement. - Skip email confirmation when set by admin or via LDAP. - Only allow users to reference groups, projects, issues, MRs, commits they have access to. +v 7.9.4 + - Security: Fix project import URL regex to prevent arbitary local repos from being imported + - Fixed issue where only 25 commits would load in file listings + - Fix LDAP identities after config update + v 7.9.3 - Contains no changes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9fe4cf7b0f6..f4472214778 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,7 +96,7 @@ The designs are made using Antetype (`.atype` files). You can use the [free Antetype viewer (Mac OSX only)] or grab an exported PNG from the design (the PNG is 1:1). -The current designs can be found in the [`gitlab1.atype` file]. +The current designs can be found in the [`gitlab8.atype` file]. ### UI development kit @@ -308,16 +308,14 @@ tests are least likely to receive timely feedback. The workflow to make a merge request is as follows: 1. Fork the project into your personal space on GitLab.com -1. Create a feature branch +1. Create a feature branch, branch away from `master`. 1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code 1. Add your changes to the [CHANGELOG](CHANGELOG) -1. If you are changing the README, some documentation or other things which - have no effect on the tests, add `[ci skip]` somewhere in the commit message - and make sure to read the [documentation styleguide][doc-styleguide] +1. If you are writing documentation, make sure to read the [documentation styleguide][doc-styleguide] 1. If you have multiple commits please combine them into one commit by [squashing them][git-squash] 1. Push the commit(s) to your fork -1. Submit a merge request (MR) to the master branch +1. Submit a merge request (MR) to the `master` branch 1. The MR title should describe the change you want to make 1. The MR description should give a motive for your change and the method you used to achieve it, see the [merge request description format] @@ -407,6 +405,7 @@ description area. Copy-paste it to retain the markdown format. entire line to follow it. This prevents linting tools from generating warnings. - Don't touch neighbouring lines. As an exception, automatic mass refactoring modifications may leave style non-compliant. +1. If the merge request adds any new libraries (gems, JavaScript libraries, etc.), they should conform to our [Licensing guidelines][license-finder-doc]. See the instructions in that document for help if your MR fails the "license-finder" test with a "Dependencies that need approval" error. ## Changes for Stable Releases @@ -532,4 +531,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide" [gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design [free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12 -[`gitlab1.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/gitlab1.atype/ +[`gitlab8.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/current/ +[license-finder-doc]: doc/development/licensing.md diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 37c2961c243..4a36342fcab 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.7.2 +3.0.0 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 39e898a4f95..8bd6ba8c5c3 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.7.1 +0.7.5 @@ -18,9 +18,8 @@ gem "mysql2", '~> 0.3.16', group: :mysql gem "pg", '~> 0.18.2', group: :postgres # Authentication libraries -gem 'devise', '~> 3.5.4' +gem 'devise', '~> 4.0' gem 'doorkeeper', '~> 3.1' -gem 'devise-async', '~> 0.9.0' gem 'omniauth', '~> 1.3.1' gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-azure-oauth2', '~> 0.0.6' @@ -36,18 +35,20 @@ gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd', '~> 2.2.0' gem 'rack-oauth2', '~> 1.2.1' +gem 'jwt' # Spam and anti-bot protection -gem 'recaptcha', require: 'recaptcha/rails' +gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails' gem 'akismet', '~> 2.0' # Two-factor authentication -gem 'devise-two-factor', '~> 2.0.0' +gem 'devise-two-factor', '~> 3.0.0' gem 'rqrcode-rails3', '~> 0.1.7' -gem 'attr_encrypted', '~> 1.3.4' +gem 'attr_encrypted', '~> 3.0.0' +gem 'u2f', '~> 0.2.1' # Browser detection -gem "browser", '~> 1.0.0' +gem "browser", '~> 2.0.3' # Extracting information from a git repository # Provide access to Gitlab::Git library @@ -72,7 +73,7 @@ gem 'grape-entity', '~> 0.4.2' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' # Pagination -gem "kaminari", "~> 0.16.3" +gem "kaminari", "~> 0.17.0" # HAML gem "haml-rails", '~> 0.9.0' @@ -83,8 +84,15 @@ gem "carrierwave", '~> 0.10.0' # Drag and Drop UI gem 'dropzonejs-rails', '~> 0.7.1' +# for backups +gem 'fog-aws', '~> 0.9' +gem 'fog-azure', '~> 0.0' +gem 'fog-core', '~> 1.40' +gem 'fog-local', '~> 0.3' +gem 'fog-google', '~> 0.3' +gem 'fog-openstack', '~> 0.1' + # for aws storage -gem "fog", "~> 1.36.0" gem "unf", '~> 0.1.4' # Authorization @@ -104,7 +112,7 @@ gem 'org-ruby', '~> 0.9.12' gem 'creole', '~> 0.5.0' gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 1.5.2' -gem 'rouge', '~> 1.10.1' +gem 'rouge', '~> 1.11' # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM @@ -120,7 +128,7 @@ group :unicorn do end # State machine -gem "state_machines-activerecord", '~> 0.3.0' +gem "state_machines-activerecord", '~> 0.4.0' # Run events after state machine commits gem 'after_commit_queue' @@ -137,7 +145,7 @@ gem 'redis-namespace' gem "httparty", '~> 0.13.3' # Colored output to console -gem "colorize", '~> 0.7.0' +gem "rainbow", '~> 2.1.0' # GitLab settings gem 'settingslogic', '~> 2.0.9' @@ -177,9 +185,6 @@ gem 'ruby-fogbugz', '~> 0.2.1' # d3 gem 'd3_rails', '~> 3.5.0' -#cal-heatmap -gem 'cal-heatmap-rails', '~> 3.6.0' - # underscore-rails gem "underscore-rails", "~> 1.8.0" @@ -197,7 +202,7 @@ gem 'licensee', '~> 8.0.0' gem "rack-attack", '~> 4.3.1' # Ace editor -gem 'ace-rails-ap', '~> 2.0.1' +gem 'ace-rails-ap', '~> 4.0.2' # Keyboard shortcuts gem 'mousetrap-rails', '~> 1.4.6' @@ -218,13 +223,13 @@ gem 'gitlab_emoji', '~> 0.3.0' gem 'gon', '~> 6.0.1' gem 'jquery-atwho-rails', '~> 1.3.2' gem 'jquery-rails', '~> 4.1.0' -gem 'jquery-scrollto-rails', '~> 1.4.3' gem 'jquery-ui-rails', '~> 5.0.0' gem 'raphael-rails', '~> 2.1.2' gem 'request_store', '~> 1.3.0' gem 'select2-rails', '~> 3.5.9' gem 'virtus', '~> 1.0.1' gem 'net-ssh', '~> 3.0.1' +gem 'base32', '~> 0.3.0' # Sentry integration gem 'sentry-raven', '~> 0.15' @@ -242,7 +247,6 @@ group :development do gem "foreman" gem 'brakeman', '~> 3.2.0', require: false - gem "annotate", "~> 2.7.0" gem 'letter_opener_web', '~> 1.3.0' gem 'quiet_assets', '~> 1.0.2' gem 'rerun', '~> 0.11.0' @@ -293,15 +297,19 @@ group :development, :test do gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-teaspoon', '~> 0.0.2' - gem 'rubocop', '~> 0.38.0', require: false + gem 'rubocop', '~> 0.40.0', require: false + gem 'rubocop-rspec', '~> 1.5.0', require: false gem 'scss_lint', '~> 0.47.0', require: false - gem 'coveralls', '~> 0.8.2', require: false + gem 'coveralls', '~> 0.8.2', require: false gem 'simplecov', '~> 0.11.0', require: false gem 'flog', require: false gem 'flay', require: false gem 'bundler-audit', require: false gem 'benchmark-ips', require: false + + gem "license_finder", require: false + gem 'knapsack' end group :test do @@ -325,8 +333,7 @@ gem "mail_room", "~> 0.7" gem 'email_reply_parser', '~> 0.5.8' ## CI -gem 'activerecord-deprecated_finders', '~> 1.0.3' -gem 'activerecord-session_store', '~> 0.1.0' +gem 'activerecord-session_store', '~> 1.0.0' gem "nested_form", '~> 0.3.2' # OAuth @@ -334,3 +341,6 @@ gem 'oauth2', '~> 1.0.0' # Soft deletion gem "paranoia", "~> 2.0" + +# Health check +gem 'health_check', '~> 1.5.1' diff --git a/Gemfile.lock b/Gemfile.lock index fe083c2b566..dfc15700494 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,8 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (2.3.2) RedCloth (4.2.9) - ace-rails-ap (2.0.1) + ace-rails-ap (4.0.2) actionmailer (4.2.6) actionpack (= 4.2.6) actionview (= 4.2.6) @@ -33,11 +32,12 @@ GEM activemodel (= 4.2.6) activesupport (= 4.2.6) arel (~> 6.0) - activerecord-deprecated_finders (1.0.4) - activerecord-session_store (0.1.2) - actionpack (>= 4.0.0, < 5) - activerecord (>= 4.0.0, < 5) - railties (>= 4.0.0, < 5) + activerecord-session_store (1.0.0) + actionpack (>= 4.0, < 5.1) + activerecord (>= 4.0, < 5.1) + multi_json (~> 1.11, >= 1.11.2) + rack (>= 1.5.2, < 3) + railties (>= 4.0, < 5.1) activesupport (4.2.6) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) @@ -51,9 +51,6 @@ GEM activerecord (>= 3.0) akismet (2.0.0) allocations (1.0.4) - annotate (2.7.0) - activerecord (>= 3.2, < 6.0) - rake (~> 10.4) arel (6.0.3) asana (0.4.0) faraday (~> 0.9) @@ -62,8 +59,8 @@ GEM oauth2 (~> 1.0) asciidoctor (1.5.3) ast (2.2.0) - attr_encrypted (1.3.4) - encryptor (>= 1.3.0) + attr_encrypted (3.0.1) + encryptor (~> 3.0.0) attr_required (1.0.0) autoprefixer-rails (6.2.3) execjs @@ -73,8 +70,24 @@ GEM descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) + azure (0.7.5) + addressable (~> 2.3) + azure-core (~> 0.1) + faraday (~> 0.9) + faraday_middleware (~> 0.10) + json (~> 1.8) + mime-types (>= 1, < 3.0) + nokogiri (~> 1.6) + systemu (~> 2.6) + thor (~> 0.19) + uuid (~> 2.0) + azure-core (0.1.2) + faraday (~> 0.9) + faraday_middleware (~> 0.10) + nokogiri (~> 1.6) babosa (1.0.2) - bcrypt (3.1.10) + base32 (0.3.2) + bcrypt (3.1.11) benchmark-ips (2.3.0) better_errors (1.0.1) coderay (>= 1.0.0) @@ -94,7 +107,7 @@ GEM sass (~> 3.0) slim (>= 1.3.6, < 4.0) terminal-table (~> 1.4) - browser (1.0.1) + browser (2.0.3) builder (3.2.2) bullet (5.0.0) activesupport (>= 3.0.0) @@ -103,7 +116,6 @@ GEM bundler (~> 1.2) thor (~> 0.18) byebug (8.2.1) - cal-heatmap-rails (3.6.0) capybara (2.6.2) addressable mime-types (>= 1.16) @@ -157,21 +169,18 @@ GEM activerecord (>= 3.2.0, < 5.0) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - devise (3.5.4) + devise (4.1.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 3.2.6, < 5) + railties (>= 4.1.0, < 5.1) responders - thread_safe (~> 0.1) warden (~> 1.2.3) - devise-async (0.9.0) - devise (~> 3.2) - devise-two-factor (2.0.1) + devise-two-factor (3.0.0) activesupport - attr_encrypted (~> 1.3.2) - devise (~> 3.5.0) + attr_encrypted (>= 1.3, < 4, != 2) + devise (~> 4.0) railties - rotp (~> 2) + rotp (~> 2.0) diff-lcs (1.2.5) diffy (3.0.7) docile (1.1.5) @@ -183,12 +192,12 @@ GEM email_spec (1.6.0) launchy (~> 2.1) mail (~> 2.2) - encryptor (1.3.0) + encryptor (3.0.0) equalizer (0.0.11) erubis (2.7.0) escape_utils (1.1.1) eventmachine (1.0.8) - excon (0.45.4) + excon (0.49.0) execjs (2.6.0) expression_parser (0.9.0) factory_girl (4.5.0) @@ -205,8 +214,6 @@ GEM multi_json ffaker (2.0.0) ffi (1.9.10) - fission (0.5.0) - CFPropertyList (~> 2.2) flay (2.6.1) ruby_parser (~> 3.0) sexp_processor (~> 4.0) @@ -216,109 +223,33 @@ GEM flowdock (0.7.1) httparty (~> 0.7) multi_json - fog (1.36.0) - fog-aliyun (>= 0.1.0) - fog-atmos - fog-aws (>= 0.6.0) - fog-brightbox (~> 0.4) - fog-core (~> 1.32) - fog-dynect (~> 0.0.2) - fog-ecloud (~> 0.1) - fog-google (<= 0.1.0) - fog-json - fog-local - fog-powerdns (>= 0.1.1) - fog-profitbricks - fog-radosgw (>= 0.0.2) - fog-riakcs - fog-sakuracloud (>= 0.0.4) - fog-serverlove - fog-softlayer - fog-storm_on_demand - fog-terremark - fog-vmfusion - fog-voxel - fog-xenserver - fog-xml (~> 0.1.1) - ipaddress (~> 0.5) - nokogiri (~> 1.5, >= 1.5.11) - fog-aliyun (0.1.0) + fog-aws (0.9.2) fog-core (~> 1.27) fog-json (~> 1.0) + fog-xml (~> 0.1) ipaddress (~> 0.8) - xml-simple (~> 1.1) - fog-atmos (0.1.0) - fog-core - fog-xml - fog-aws (0.8.1) + fog-azure (0.0.2) + azure (~> 0.6) fog-core (~> 1.27) fog-json (~> 1.0) fog-xml (~> 0.1) - ipaddress (~> 0.8) - fog-brightbox (0.10.1) - fog-core (~> 1.22) - fog-json - inflecto (~> 0.0.2) - fog-core (1.35.0) + fog-core (1.40.0) builder - excon (~> 0.45) + excon (~> 0.49) formatador (~> 0.2) - fog-dynect (0.0.2) - fog-core - fog-json - fog-xml - fog-ecloud (0.3.0) - fog-core - fog-xml - fog-google (0.1.0) + fog-google (0.3.2) fog-core fog-json fog-xml fog-json (1.0.2) fog-core (~> 1.0) multi_json (~> 1.10) - fog-local (0.2.1) - fog-core (~> 1.27) - fog-powerdns (0.1.1) + fog-local (0.3.0) fog-core (~> 1.27) - fog-json (~> 1.0) - fog-xml (~> 0.1) - fog-profitbricks (0.0.5) - fog-core - fog-xml - nokogiri - fog-radosgw (0.0.5) - fog-core (>= 1.21.0) - fog-json - fog-xml (>= 0.0.1) - fog-riakcs (0.1.0) - fog-core - fog-json - fog-xml - fog-sakuracloud (1.7.5) - fog-core - fog-json - fog-serverlove (0.1.2) - fog-core - fog-json - fog-softlayer (1.0.3) - fog-core - fog-json - fog-storm_on_demand (0.1.1) - fog-core - fog-json - fog-terremark (0.1.0) - fog-core - fog-xml - fog-vmfusion (0.1.0) - fission - fog-core - fog-voxel (0.1.0) - fog-core - fog-xml - fog-xenserver (0.2.2) - fog-core - fog-xml + fog-openstack (0.1.6) + fog-core (>= 1.39) + fog-json (>= 1.0) + ipaddress (>= 0.8) fog-xml (0.1.2) fog-core nokogiri (~> 1.5, >= 1.5.11) @@ -405,6 +336,8 @@ GEM html2haml (>= 1.0.1) railties (>= 4.0.1) hashie (3.4.3) + health_check (1.5.1) + rails (>= 2.3.0) highline (1.7.8) hipchat (1.5.2) httparty @@ -425,18 +358,15 @@ GEM httpclient (2.7.0.1) i18n (0.7.0) ice_nine (0.11.1) - inflecto (0.0.2) influxdb (0.2.3) cause json - ipaddress (0.8.2) + ipaddress (0.8.3) jquery-atwho-rails (1.3.2) jquery-rails (4.1.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jquery-scrollto-rails (1.4.3) - railties (> 3.1, < 5.0) jquery-turbolinks (2.1.0) railties (>= 3.1.0) turbolinks @@ -444,10 +374,13 @@ GEM railties (>= 3.2.16) json (1.8.3) jwt (1.5.2) - kaminari (0.16.3) + kaminari (0.17.0) actionpack (>= 3.0.0) activesupport (>= 3.0.0) kgio (2.10.0) + knapsack (1.11.0) + rake + timecop (>= 0.1.0) launchy (2.4.3) addressable (~> 2.3) letter_opener (1.4.1) @@ -456,6 +389,12 @@ GEM actionmailer (>= 3.2) letter_opener (~> 1.0) railties (>= 3.2) + license_finder (2.1.0) + bundler + httparty + rubyzip + thor + xml-simple licensee (8.0.0) rugged (>= 0.24b) listen (3.0.5) @@ -471,7 +410,7 @@ GEM method_source (0.8.2) mime-types (2.99.1) mimemagic (0.3.0) - mini_portile2 (2.0.0) + mini_portile2 (2.1.0) minitest (5.7.0) mousetrap-rails (1.4.6) multi_json (1.11.2) @@ -482,8 +421,9 @@ GEM net-ldap (0.12.1) net-ssh (3.0.1) newrelic_rpm (3.14.1.311) - nokogiri (1.6.7.2) - mini_portile2 (~> 2.0.0.rc2) + nokogiri (1.6.8) + mini_portile2 (~> 2.1.0) + pkg-config (~> 1.1.7) oauth (0.4.7) oauth2 (1.0.0) faraday (>= 0.8, < 0.10) @@ -552,9 +492,10 @@ GEM orm_adapter (0.5.0) paranoia (2.1.4) activerecord (~> 4.0) - parser (2.3.0.6) + parser (2.3.1.0) ast (~> 2.2) pg (0.18.4) + pkg-config (1.1.7) poltergeist (1.9.0) capybara (~> 2.1) cliver (~> 0.3.1) @@ -630,7 +571,7 @@ GEM debugger-ruby_core_source (~> 1.3) rdoc (3.12.2) json (~> 1.4) - recaptcha (1.0.2) + recaptcha (3.0.0) json redcarpet (3.3.3) redis (3.3.0) @@ -658,8 +599,8 @@ GEM responders (2.1.1) railties (>= 4.2.0, < 5.1) rinku (1.7.3) - rotp (2.1.1) - rouge (1.10.1) + rotp (2.1.2) + rouge (1.11.0) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) @@ -687,15 +628,17 @@ GEM rspec-retry (0.4.5) rspec-core rspec-support (3.4.1) - rubocop (0.38.0) - parser (>= 2.3.0.6, < 3.0) + rubocop (0.40.0) + parser (>= 2.3.1.0, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) + rubocop-rspec (1.5.0) + rubocop (>= 0.40.0) ruby-fogbugz (0.2.1) crack (~> 0.4) - ruby-progressbar (1.7.5) + ruby-progressbar (1.8.1) ruby-saml (1.1.2) nokogiri (>= 1.5.10) uuid (~> 2.3) @@ -706,6 +649,7 @@ GEM sexp_processor (~> 4.1) rubyntlm (0.5.2) rubypants (0.2.0) + rubyzip (1.2.0) rufus-scheduler (3.1.10) rugged (0.24.0) safe_yaml (1.0.4) @@ -789,11 +733,11 @@ GEM activesupport (>= 4.0) sprockets (>= 3.0.0) state_machines (0.4.0) - state_machines-activemodel (0.3.0) - activemodel (~> 4.1) + state_machines-activemodel (0.4.0) + activemodel (>= 4.1, < 5.1) state_machines (>= 0.4.0) - state_machines-activerecord (0.3.0) - activerecord (~> 4.1) + state_machines-activerecord (0.4.0) + activerecord (>= 4.1, < 5.1) state_machines-activemodel (>= 0.3.0) stringex (2.5.2) systemu (2.6.5) @@ -816,6 +760,7 @@ GEM thor (0.19.1) thread_safe (0.3.5) tilt (2.0.2) + timecop (0.8.1) timfel-krb5-auth (0.8.3) tinder (1.10.1) eventmachine (~> 1.0) @@ -835,6 +780,7 @@ GEM simple_oauth (~> 0.1.4) tzinfo (1.2.2) thread_safe (~> 0.1) + u2f (0.2.1) uglifier (2.7.2) execjs (>= 0.3.0) json (>= 1.8.0) @@ -842,7 +788,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.2) - unicode-display_width (1.0.2) + unicode-display_width (1.0.5) unicorn (4.9.0) kgio (~> 2.6) rack @@ -859,7 +805,7 @@ GEM coercible (~> 1.0) descendants_tracker (~> 0.0, >= 0.0.3) equalizer (~> 0.0, >= 0.0.9) - warden (1.2.4) + warden (1.2.6) rack (>= 1.0) web-console (2.3.0) activemodel (>= 4.0) @@ -885,45 +831,41 @@ PLATFORMS DEPENDENCIES RedCloth (~> 4.2.9) - ace-rails-ap (~> 2.0.1) - activerecord-deprecated_finders (~> 1.0.3) - activerecord-session_store (~> 0.1.0) + ace-rails-ap (~> 4.0.2) + activerecord-session_store (~> 1.0.0) acts-as-taggable-on (~> 3.4) addressable (~> 2.3.8) after_commit_queue akismet (~> 2.0) allocations (~> 1.0) - annotate (~> 2.7.0) asana (~> 0.4.0) asciidoctor (~> 1.5.2) - attr_encrypted (~> 1.3.4) + attr_encrypted (~> 3.0.0) awesome_print (~> 1.2.0) babosa (~> 1.0.2) + base32 (~> 0.3.0) benchmark-ips better_errors (~> 1.0.1) binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.3.0) brakeman (~> 3.2.0) - browser (~> 1.0.0) + browser (~> 2.0.3) bullet bundler-audit byebug - cal-heatmap-rails (~> 3.6.0) capybara (~> 2.6.2) capybara-screenshot (~> 1.0.0) carrierwave (~> 0.10.0) charlock_holmes (~> 0.7.3) coffee-rails (~> 4.1.0) - colorize (~> 0.7.0) connection_pool (~> 2.0) coveralls (~> 0.8.2) creole (~> 0.5.0) d3_rails (~> 3.5.0) database_cleaner (~> 1.4.0) default_value_for (~> 3.0.0) - devise (~> 3.5.4) - devise-async (~> 0.9.0) - devise-two-factor (~> 2.0.0) + devise (~> 4.0) + devise-two-factor (~> 3.0.0) diffy (~> 3.0.3) doorkeeper (~> 3.1) dropzonejs-rails (~> 0.7.1) @@ -933,7 +875,12 @@ DEPENDENCIES ffaker (~> 2.0.0) flay flog - fog (~> 1.36.0) + fog-aws (~> 0.9) + fog-azure (~> 0.0) + fog-core (~> 1.40) + fog-google (~> 0.3) + fog-local (~> 0.3) + fog-openstack (~> 0.1) font-awesome-rails (~> 4.2) foreman fuubar (~> 2.0.0) @@ -951,17 +898,20 @@ DEPENDENCIES grape (~> 0.13.0) grape-entity (~> 0.4.2) haml-rails (~> 0.9.0) + health_check (~> 1.5.1) hipchat (~> 1.5.0) html-pipeline (~> 1.11.0) httparty (~> 0.13.3) influxdb (~> 0.2) jquery-atwho-rails (~> 1.3.2) jquery-rails (~> 4.1.0) - jquery-scrollto-rails (~> 1.4.3) jquery-turbolinks (~> 2.1.0) jquery-ui-rails (~> 5.0.0) - kaminari (~> 0.16.3) + jwt + kaminari (~> 0.17.0) + knapsack letter_opener_web (~> 1.3.0) + license_finder licensee (~> 8.0.0) loofah (~> 2.0.3) mail_room (~> 0.7) @@ -1001,10 +951,11 @@ DEPENDENCIES rack-oauth2 (~> 1.2.1) rails (= 4.2.6) rails-deprecated_sanitizer (~> 1.0.3) + rainbow (~> 2.1.0) raphael-rails (~> 2.1.2) rblineprof rdoc (~> 3.6) - recaptcha + recaptcha (~> 3.0) redcarpet (~> 3.3.3) redis (~> 3.2) redis-namespace @@ -1012,11 +963,12 @@ DEPENDENCIES request_store (~> 1.3.0) rerun (~> 0.11.0) responders (~> 2.0) - rouge (~> 1.10.1) + rouge (~> 1.11) rqrcode-rails3 (~> 0.1.7) rspec-rails (~> 3.4.0) rspec-retry - rubocop (~> 0.38.0) + rubocop (~> 0.40.0) + rubocop-rspec (~> 1.5.0) ruby-fogbugz (~> 0.2.1) sanitize (~> 2.0) sass-rails (~> 5.0.0) @@ -1041,7 +993,7 @@ DEPENDENCIES spring-commands-spinach (~> 1.1.0) spring-commands-teaspoon (~> 0.0.2) sprockets (~> 3.6.0) - state_machines-activerecord (~> 0.3.0) + state_machines-activerecord (~> 0.4.0) task_list (~> 1.0.2) teaspoon (~> 1.1.0) teaspoon-jasmine (~> 2.2.0) @@ -1049,6 +1001,7 @@ DEPENDENCIES thin (~> 1.6.1) tinder (~> 1.10.0) turbolinks (~> 2.5.0) + u2f (~> 0.2.1) uglifier (~> 2.7.2) underscore-rails (~> 1.8.0) unf (~> 0.1.4) @@ -1061,4 +1014,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.12.1 + 1.12.5 diff --git a/README.md b/README.md index 5e41665a6ba..fee93d5f9c3 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ # GitLab [![build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) -[![Build Status](https://semaphoreci.com/api/v1/projects/2f1a5809-418b-4cc2-a1f4-819607579fe7/400484/shields_badge.svg)](https://semaphoreci.com/gitlabhq/gitlabhq) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) -[![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.svg?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master) ## Canonical source @@ -35,11 +33,11 @@ There are two editions of GitLab: On [about.gitlab.com](https://about.gitlab.com/) you can find more information about: -- [Subscriptions](https://about.gitlab.com/subscription/) +- [Subscriptions](https://about.gitlab.com/pricing/) - [Consultancy](https://about.gitlab.com/consultancy/) - [Community](https://about.gitlab.com/community/) - [Hosted GitLab.com](https://about.gitlab.com/gitlab-com/) use GitLab as a free service -- [GitLab Enterprise Edition](https://about.gitlab.com/gitlab-ee/) with additional features aimed at larger organizations. +- [GitLab Enterprise Edition](https://about.gitlab.com/features/#enterprise) with additional features aimed at larger organizations. - [GitLab CI](https://about.gitlab.com/gitlab-ci/) a continuous integration (CI) server that is easy to integrate with GitLab. ## Requirements @@ -8,3 +8,5 @@ relative_url_conf = File.expand_path('../config/initializers/relative_url', __FI require relative_url_conf if File.exist?("#{relative_url_conf}.rb") Gitlab::Application.load_tasks + +Knapsack.load_tasks if defined?(Knapsack) @@ -1 +1 @@ -8.8.0-pre +8.9.0-pre diff --git a/app/assets/images/ci/arch.jpg b/app/assets/images/ci/arch.jpg Binary files differdeleted file mode 100644 index 0e05674e840..00000000000 --- a/app/assets/images/ci/arch.jpg +++ /dev/null diff --git a/app/assets/images/ci/favicon.ico b/app/assets/images/ci/favicon.ico Binary files differdeleted file mode 100644 index 9663d4d00b9..00000000000 --- a/app/assets/images/ci/favicon.ico +++ /dev/null diff --git a/app/assets/images/ci/loader.gif b/app/assets/images/ci/loader.gif Binary files differdeleted file mode 100644 index 2fcb8f2da0d..00000000000 --- a/app/assets/images/ci/loader.gif +++ /dev/null diff --git a/app/assets/images/ci/no_avatar.png b/app/assets/images/ci/no_avatar.png Binary files differdeleted file mode 100644 index 752d26adba7..00000000000 --- a/app/assets/images/ci/no_avatar.png +++ /dev/null diff --git a/app/assets/images/ci/rails.png b/app/assets/images/ci/rails.png Binary files differdeleted file mode 100644 index d5edc04e65f..00000000000 --- a/app/assets/images/ci/rails.png +++ /dev/null diff --git a/app/assets/images/ci/service_sample.png b/app/assets/images/ci/service_sample.png Binary files differdeleted file mode 100644 index 65d29e3fd89..00000000000 --- a/app/assets/images/ci/service_sample.png +++ /dev/null diff --git a/app/assets/images/mailers/gitlab_header_logo.png b/app/assets/images/mailers/gitlab_header_logo.png Binary files differnew file mode 100644 index 00000000000..35ca1860887 --- /dev/null +++ b/app/assets/images/mailers/gitlab_header_logo.png diff --git a/app/assets/images/mailers/gitlab_tanuki_2x.png b/app/assets/images/mailers/gitlab_tanuki_2x.png Binary files differnew file mode 100644 index 00000000000..551dd6ce2ce --- /dev/null +++ b/app/assets/images/mailers/gitlab_tanuki_2x.png diff --git a/app/assets/javascripts/LabelManager.js.coffee b/app/assets/javascripts/LabelManager.js.coffee new file mode 100644 index 00000000000..365a062bb81 --- /dev/null +++ b/app/assets/javascripts/LabelManager.js.coffee @@ -0,0 +1,84 @@ +class @LabelManager + errorMessage: 'Unable to update label prioritization at this time' + + constructor: (opts = {}) -> + # Defaults + { + @togglePriorityButton = $('.js-toggle-priority') + @prioritizedLabels = $('.js-prioritized-labels') + @otherLabels = $('.js-other-labels') + } = opts + + @prioritizedLabels.sortable( + items: 'li' + placeholder: 'list-placeholder' + axis: 'y' + update: @onPrioritySortUpdate.bind(@) + ) + + @bindEvents() + + bindEvents: -> + @togglePriorityButton.on 'click', @, @onTogglePriorityClick + + onTogglePriorityClick: (e) -> + e.preventDefault() + _this = e.data + $btn = $(e.currentTarget) + $label = $("##{$btn.data('domId')}") + action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add' + _this.toggleLabelPriority($label, action) + + toggleLabelPriority: ($label, action, persistState = true) -> + _this = @ + url = $label.find('.js-toggle-priority').data 'url' + + $target = @prioritizedLabels + $from = @otherLabels + + # Optimistic update + if action is 'remove' + $target = @otherLabels + $from = @prioritizedLabels + + if $from.find('li').length is 1 + $from.find('.empty-message').show() + + if not $target.find('li').length + $target.find('.empty-message').hide() + + $label.detach().appendTo($target) + + # Return if we are not persisting state + return unless persistState + + if action is 'remove' + xhr = $.ajax url: url, type: 'DELETE' + else + xhr = @savePrioritySort($label, action) + + xhr.fail @rollbackLabelPosition.bind(@, $label, action) + + onPrioritySortUpdate: -> + xhr = @savePrioritySort() + + xhr.fail -> + new Flash(@errorMessage, 'alert') + + savePrioritySort: () -> + $.post + url: @prioritizedLabels.data('url') + data: + label_ids: @getSortedLabelsIds() + + rollbackLabelPosition: ($label, originalAction)-> + action = if originalAction is 'remove' then 'add' else 'remove' + @toggleLabelPriority($label, action, false) + + new Flash(@errorMessage, 'alert') + + getSortedLabelsIds: -> + sortedIds = [] + @prioritizedLabels.find('li').each -> + sortedIds.push $(@).data 'id' + sortedIds diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee index 5092e824e65..ed5a5d0260c 100644 --- a/app/assets/javascripts/activities.js.coffee +++ b/app/assets/javascripts/activities.js.coffee @@ -1,11 +1,14 @@ class @Activities constructor: -> - Pager.init 20, true + Pager.init 20, true, false, @updateTooltips $(".event-filter-link").on "click", (event) => event.preventDefault() @toggleFilter($(event.currentTarget)) @reloadActivities() + updateTooltips: -> + gl.utils.localTimeAgo($('.js-timeago', '#activity')) + reloadActivities: -> $(".content_list").html '' Pager.init 20, true diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index dd1bbb37551..3f61ea1eaf4 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -1,14 +1,15 @@ @Api = - groups_path: "/api/:version/groups.json" - group_path: "/api/:version/groups/:id.json" - namespaces_path: "/api/:version/namespaces.json" - group_projects_path: "/api/:version/groups/:id/projects.json" - projects_path: "/api/:version/projects.json" - labels_path: "/api/:version/projects/:id/labels" - license_path: "/api/:version/licenses/:key" + groupsPath: "/api/:version/groups.json" + groupPath: "/api/:version/groups/:id.json" + namespacesPath: "/api/:version/namespaces.json" + groupProjectsPath: "/api/:version/groups/:id/projects.json" + projectsPath: "/api/:version/projects.json" + labelsPath: "/api/:version/projects/:id/labels" + licensePath: "/api/:version/licenses/:key" + gitignorePath: "/api/:version/gitignores/:key" group: (group_id, callback) -> - url = Api.buildUrl(Api.group_path) + url = Api.buildUrl(Api.groupPath) url = url.replace(':id', group_id) $.ajax( @@ -22,7 +23,7 @@ # Return groups list. Filtered by query # Only active groups retrieved groups: (query, skip_ldap, callback) -> - url = Api.buildUrl(Api.groups_path) + url = Api.buildUrl(Api.groupsPath) $.ajax( url: url @@ -36,7 +37,7 @@ # Return namespaces list. Filtered by query namespaces: (query, callback) -> - url = Api.buildUrl(Api.namespaces_path) + url = Api.buildUrl(Api.namespacesPath) $.ajax( url: url @@ -50,7 +51,7 @@ # Return projects list. Filtered by query projects: (query, order, callback) -> - url = Api.buildUrl(Api.projects_path) + url = Api.buildUrl(Api.projectsPath) $.ajax( url: url @@ -64,7 +65,7 @@ callback(projects) newLabel: (project_id, data, callback) -> - url = Api.buildUrl(Api.labels_path) + url = Api.buildUrl(Api.labelsPath) url = url.replace(':id', project_id) data.private_token = gon.api_token @@ -80,7 +81,7 @@ # Return group projects list. Filtered by query groupProjects: (group_id, query, callback) -> - url = Api.buildUrl(Api.group_projects_path) + url = Api.buildUrl(Api.groupProjectsPath) url = url.replace(':id', group_id) $.ajax( @@ -95,7 +96,7 @@ # Return text for a specific license licenseText: (key, data, callback) -> - url = Api.buildUrl(Api.license_path).replace(':key', key) + url = Api.buildUrl(Api.licensePath).replace(':key', key) $.ajax( url: url @@ -103,6 +104,12 @@ ).done (license) -> callback(license) + gitignoreText: (key, callback) -> + url = Api.buildUrl(Api.gitignorePath).replace(':key', key) + + $.get url, (gitignore) -> + callback(gitignore) + buildUrl: (url) -> url = gon.relative_url_root + url if gon.relative_url_root? return url.replace(':version', gon.api_version) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 5bac8eef1cb..69d4c4f5dd3 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -4,7 +4,7 @@ # It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the # the compiled file. # -#= require jquery +#= require jquery2 #= require jquery-ui/autocomplete #= require jquery-ui/datepicker #= require jquery-ui/draggable @@ -18,8 +18,6 @@ #= require jquery.atwho #= require jquery.scrollTo #= require jquery.turbolinks -#= require d3 -#= require cal-heatmap #= require turbolinks #= require autosave #= require bootstrap/affix @@ -37,7 +35,6 @@ #= require raphael #= require g.raphael #= require g.bar -#= require Chart #= require branch-graph #= require ace/ace #= require ace/ext-searchbox @@ -52,9 +49,17 @@ #= require shortcuts_network #= require jquery.nicescroll #= require date.format -#= require_tree . +#= require_directory ./behaviors +#= require_directory ./blob +#= require_directory ./ci +#= require_directory ./commit +#= require_directory ./extensions +#= require_directory ./lib +#= require_directory ./u2f +#= require_directory . #= require fuzzaldrin-plus #= require cropper +#= require u2f window.slugify = (text) -> text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() @@ -157,19 +162,6 @@ $ -> $el.data('placement') || 'bottom' ) - $('.header-logo .home').tooltip( - placement: (_, el) -> - $el = $(el) - if $('.page-with-sidebar').hasClass('page-sidebar-collapsed') then 'right' else 'bottom' - container: 'body' - ) - - $('.page-with-sidebar').tooltip( - selector: '.sidebar-collapsed .nav-sidebar a, .sidebar-collapsed a.sidebar-user' - placement: 'right' - container: 'body' - ) - # Form submitter $('.trigger-submit').on 'change', -> $(@).parents('form').submit() @@ -202,8 +194,10 @@ $ -> $('.navbar-toggle').on 'click', -> $('.header-content .title').toggle() + $('.header-content .header-logo').toggle() $('.header-content .navbar-collapse').toggle() $('.navbar-toggle').toggleClass('active') + $('.navbar-toggle i').toggleClass("fa-angle-right fa-angle-left") # Show/hide comments on diff $("body").on "click", ".js-toggle-diff-comments", (e) -> @@ -219,6 +213,10 @@ $ -> form = btn.closest("form") new ConfirmDangerModal(form, text) + + $(document).on 'click', 'button', -> + $(this).blur() + $('input[type="search"]').each -> $this = $(this) $this.attr 'value', $this.val() @@ -231,7 +229,6 @@ $ -> $this.attr 'value', $this.val() $sidebarGutterToggle = $('.js-sidebar-toggle') - $navIconToggle = $('.toggle-nav-collapse') $(document) .off 'breakpoint:change' @@ -241,42 +238,6 @@ $ -> if $gutterIcon.hasClass('fa-angle-double-right') $sidebarGutterToggle.trigger('click') - $navIcon = $navIconToggle.find('.fa') - if $navIcon.hasClass('fa-angle-left') - $navIconToggle.trigger('click') - - $(document) - .off 'click', '.js-sidebar-toggle' - .on 'click', '.js-sidebar-toggle', (e, triggered) -> - e.preventDefault() - $this = $(this) - $thisIcon = $this.find 'i' - $allGutterToggleIcons = $('.js-sidebar-toggle i') - if $thisIcon.hasClass('fa-angle-double-right') - $allGutterToggleIcons - .removeClass('fa-angle-double-right') - .addClass('fa-angle-double-left') - $('aside.right-sidebar') - .removeClass('right-sidebar-expanded') - .addClass('right-sidebar-collapsed') - $('.page-with-sidebar') - .removeClass('right-sidebar-expanded') - .addClass('right-sidebar-collapsed') - else - $allGutterToggleIcons - .removeClass('fa-angle-double-left') - .addClass('fa-angle-double-right') - $('aside.right-sidebar') - .removeClass('right-sidebar-collapsed') - .addClass('right-sidebar-expanded') - $('.page-with-sidebar') - .removeClass('right-sidebar-collapsed') - .addClass('right-sidebar-expanded') - if not triggered - $.cookie("collapsed_gutter", - $('.right-sidebar') - .hasClass('right-sidebar-collapsed'), { path: '/' }) - fitSidebarForSize = -> oldBootstrapBreakpoint = bootstrapBreakpoint bootstrapBreakpoint = bp.getBreakpointSize() @@ -289,9 +250,10 @@ $ -> $(document).trigger('breakpoint:change', [bootstrapBreakpoint]) $(window) - .off "resize" - .on "resize", (e) -> + .off "resize.app" + .on "resize.app", (e) -> fitSidebarForSize() + gl.awardsHandler = new AwardsHandler() checkInitialSidebarSize() new Aside() diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index bf95e06b4e5..136db8ee14d 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,201 +1,354 @@ class @AwardsHandler - constructor: (@getEmojisUrl, @postEmojiUrl, @noteableType, @noteableId, @unicodes) -> - $('.js-add-award').on 'click', (event) => - event.stopPropagation() - event.preventDefault() - @showEmojiMenu() + constructor: -> - $('html').on 'click', (event) -> - if !$(event.target).closest('.emoji-menu').length + @aliases = gl.emojiAliases() + + $(document) + .off 'click', '.js-add-award' + .on 'click', '.js-add-award', (e) => + e.stopPropagation() + e.preventDefault() + + @showEmojiMenu $(e.currentTarget) + + $('html').on 'click', (e) -> + $target = $ e.target + + unless $target.closest('.emoji-menu-content').length + $('.js-awards-block.current').removeClass 'current' + + unless $target.closest('.emoji-menu').length if $('.emoji-menu').is(':visible') + $('.js-add-award.is-active').removeClass 'is-active' $('.emoji-menu').removeClass 'is-visible' - $('.awards') - .off 'click' - .on 'click', '.js-emoji-btn', @handleClick + $(document) + .off 'click', '.js-emoji-btn' + .on 'click', '.js-emoji-btn', (e) => + e.preventDefault() - @renderFrequentlyUsedBlock() + $target = $ e.currentTarget + emoji = $target.find('.icon').data 'emoji' - handleClick: (e) -> - e.preventDefault() - emoji = $(this) - .find('.icon') - .data 'emoji' + $target.closest('.js-awards-block').addClass 'current' + @addAward @getVotesBlock(), @getAwardUrl(), emoji - if emoji is 'thumbsup' and awardsHandler.didUserClickEmoji $(this), 'thumbsdown' - awardsHandler.addAward 'thumbsdown' - else if emoji is 'thumbsdown' and awardsHandler.didUserClickEmoji $(this), 'thumbsup' - awardsHandler.addAward 'thumbsup' + showEmojiMenu: ($addBtn) -> - awardsHandler.addAward emoji + $menu = $ '.emoji-menu' - $(this).trigger 'blur' + if $addBtn.hasClass 'js-note-emoji' + $addBtn.parents('.note').find('.js-awards-block').addClass 'current' + else + $addBtn.closest('.js-awards-block').addClass 'current' - didUserClickEmoji: (that, emoji) -> - if $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title') - $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title').indexOf('me') > -1 + if $menu.length + $holder = $addBtn.closest('.js-award-holder') - showEmojiMenu: -> - if $('.emoji-menu').length - if $('.emoji-menu').is '.is-visible' - $('.emoji-menu').removeClass 'is-visible' + if $menu.is '.is-visible' + $addBtn.removeClass 'is-active' + $menu.removeClass 'is-visible' $('#emoji_search').blur() else - $('.emoji-menu').addClass 'is-visible' + $addBtn.addClass 'is-active' + @positionMenu($menu, $addBtn) + + $menu.addClass 'is-visible' $('#emoji_search').focus() else - $('.js-add-award').addClass 'is-loading' - $.get @getEmojisUrl, (response) => - $('.js-add-award').removeClass 'is-loading' - $('.js-award-holder').append response + $addBtn.addClass 'is-loading is-active' + url = @getAwardMenuUrl() + + @createEmojiMenu url, => + $addBtn.removeClass 'is-loading' + $menu = $('.emoji-menu') + @positionMenu($menu, $addBtn) + @renderFrequentlyUsedBlock() unless @frequentEmojiBlockRendered + setTimeout => - $('.emoji-menu').addClass 'is-visible' + $menu.addClass 'is-visible' $('#emoji_search').focus() @setupSearch() , 200 - addAward: (emoji) -> - @postEmoji emoji, => - @addAwardToEmojiBar(emoji) + + createEmojiMenu: (awardMenuUrl, callback) -> + + $.get awardMenuUrl, (response) -> + $('body').append response + callback() + + + positionMenu: ($menu, $addBtn) -> + + position = $addBtn.data('position') + + # The menu could potentially be off-screen or in a hidden overflow element + # So we position the element absolute in the body + css = + top: "#{$addBtn.offset().top + $addBtn.outerHeight()}px" + + if position? and position is 'right' + css.left = "#{($addBtn.offset().left - $menu.outerWidth()) + 20}px" + $menu.addClass 'is-aligned-right' + else + css.left = "#{$addBtn.offset().left}px" + $menu.removeClass 'is-aligned-right' + + $menu.css(css) + + + addAward: (votesBlock, awardUrl, emoji, checkMutuality = true, callback) -> + + emoji = @normilizeEmojiName emoji + + @postEmoji awardUrl, emoji, => + @addAwardToEmojiBar votesBlock, emoji, checkMutuality + callback?() $('.emoji-menu').removeClass 'is-visible' - addAwardToEmojiBar: (emoji) -> - @addEmojiToFrequentlyUsedList(emoji) - if @exist(emoji) - if @isActive(emoji) - @decrementCounter(emoji) + addAwardToEmojiBar: (votesBlock, emoji, checkForMutuality = true) -> + + @checkMutuality votesBlock, emoji if checkForMutuality + @addEmojiToFrequentlyUsedList emoji + + emoji = @normilizeEmojiName emoji + $emojiButton = @findEmojiIcon(votesBlock, emoji).parent() + + if $emojiButton.length > 0 + if @isActive $emojiButton + @decrementCounter $emojiButton, emoji else - counter = @findEmojiIcon(emoji).siblings('.js-counter') - counter.text(parseInt(counter.text()) + 1) - counter.parent().addClass('active') - @addMeToAuthorList(emoji) + counter = $emojiButton.find '.js-counter' + counter.text parseInt(counter.text()) + 1 + $emojiButton.addClass 'active' + @addMeToUserList votesBlock, emoji + @animateEmoji $emojiButton else - @createEmoji(emoji) - - exist: (emoji) -> - @findEmojiIcon(emoji).length > 0 - - isActive: (emoji) -> - @findEmojiIcon(emoji).parent().hasClass('active') - - decrementCounter: (emoji) -> - counter = @findEmojiIcon(emoji).siblings('.js-counter') - emojiIcon = counter.parent() - if parseInt(counter.text()) > 1 - counter.text(parseInt(counter.text()) - 1) - emojiIcon.removeClass('active') - @removeMeFromAuthorList(emoji) - else if emoji == 'thumbsup' || emoji == 'thumbsdown' - emojiIcon.tooltip('destroy') - counter.text(0) - emojiIcon.removeClass('active') - @removeMeFromAuthorList(emoji) + votesBlock.removeClass 'hidden' + @createEmoji votesBlock, emoji + + + getVotesBlock: -> + + currentBlock = $ '.js-awards-block.current' + return if currentBlock.length then currentBlock else $('.js-awards-block').eq 0 + + + getAwardUrl: -> return @getVotesBlock().data 'award-url' + + + checkMutuality: (votesBlock, emoji) -> + + awardUrl = @getAwardUrl() + + if emoji in [ 'thumbsup', 'thumbsdown' ] + mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup' + $emojiButton = votesBlock.find("[data-emoji=#{mutualVote}]").parent() + isAlreadyVoted = $emojiButton.hasClass 'active' + + if isAlreadyVoted + @showEmojiLoader $emojiButton + @addAward votesBlock, awardUrl, mutualVote, false, -> + $emojiButton.removeClass 'is-loading' + + + showEmojiLoader: ($emojiButton) -> + + $loader = $emojiButton.find '.fa-spinner' + + unless $loader.length + $emojiButton.append '<i class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading"></i>' + + $emojiButton.addClass 'is-loading' + + + isActive: ($emojiButton) -> $emojiButton.hasClass 'active' + + + decrementCounter: ($emojiButton, emoji) -> + + counter = $ '.js-counter', $emojiButton + counterNumber = parseInt counter.text(), 10 + + if counterNumber > 1 + counter.text counterNumber - 1 + @removeMeFromUserList $emojiButton, emoji + else if emoji is 'thumbsup' or emoji is 'thumbsdown' + $emojiButton.tooltip 'destroy' + counter.text '0' + @removeMeFromUserList $emojiButton, emoji + @removeEmoji $emojiButton if $emojiButton.parents('.note').length else - emojiIcon.tooltip('destroy') - emojiIcon.remove() - - removeMeFromAuthorList: (emoji) -> - awardBlock = @findEmojiIcon(emoji).parent() - authors = awardBlock - .attr('data-original-title') - .split(', ') - authors.splice(authors.indexOf('me'),1) + @removeEmoji $emojiButton + + $emojiButton.removeClass 'active' + + + removeEmoji: ($emojiButton) -> + + $emojiButton.tooltip('destroy') + $emojiButton.remove() + + $votesBlock = @getVotesBlock() + + if $votesBlock.find('.js-emoji-btn').length is 0 + $votesBlock.addClass 'hidden' + + + getAwardTooltip: ($awardBlock) -> + + return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title') or '' + + + removeMeFromUserList: ($emojiButton, emoji) -> + + awardBlock = $emojiButton + originalTitle = @getAwardTooltip awardBlock + + authors = originalTitle.split ', ' + authors.splice authors.indexOf('me'), 1 + + newAuthors = authors.join ', ' + awardBlock - .closest('.js-emoji-btn') - .attr('data-original-title', authors.join(', ')) - @resetTooltip(awardBlock) - - addMeToAuthorList: (emoji) -> - awardBlock = @findEmojiIcon(emoji).parent() - origTitle = awardBlock.attr('data-original-title').trim() - authors = [] + .closest '.js-emoji-btn' + .removeData 'original-title' + .attr 'data-original-title', newAuthors + + @resetTooltip awardBlock + + + addMeToUserList: (votesBlock, emoji) -> + + awardBlock = @findEmojiIcon(votesBlock, emoji).parent() + origTitle = @getAwardTooltip awardBlock + users = [] + if origTitle - authors = origTitle.split(', ') - authors.push('me') - awardBlock.attr('data-original-title', authors.join(', ')) - @resetTooltip(awardBlock) + users = origTitle.trim().split ', ' + + users.push 'me' + awardBlock.attr 'title', users.join ', ' + + @resetTooltip awardBlock + resetTooltip: (award) -> - award.tooltip('destroy') - # "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. - setTimeout (-> - award.tooltip() - ), 200 + award.tooltip 'destroy' + + # 'destroy' call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. + cb = -> award.tooltip() + setTimeout cb, 200 + + createEmoji_: (votesBlock, emoji) -> - createEmoji: (emoji) -> - emojiCssClass = @resolveNameToCssClass(emoji) + emojiCssClass = @resolveNameToCssClass emoji + buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'> + <div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div> + <span class='award-control-text js-counter'>1</span> + </button>" - nodes = [] - nodes.push( - "<button class='btn award-control js-emoji-btn has-tooltip active' data-original-title='me'>", - "<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>", - "<span class='award-control-text js-counter'>1</span>", - "</button>" - ) + $emojiButton = $ buttonHtml + $emojiButton + .insertBefore votesBlock.find '.js-award-holder' + .find '.emoji-icon' + .data 'emoji', emoji - $(nodes.join("\n")) - .insertBefore('.js-award-holder') - .find('.emoji-icon') - .data('emoji', emoji) + @animateEmoji $emojiButton $('.award-control').tooltip() + votesBlock.removeClass 'current' + + + animateEmoji: ($emoji) -> + + className = 'pulse animated' + + $emoji.addClass className + setTimeout (-> $emoji.removeClass className), 321 + + + createEmoji: (votesBlock, emoji) -> + + if $('.emoji-menu').length + return @createEmoji_ votesBlock, emoji + + @createEmojiMenu @getAwardMenuUrl(), => @createEmoji_ votesBlock, emoji + + + getAwardMenuUrl: -> return gon.award_menu_url + resolveNameToCssClass: (emoji) -> - emojiIcon = $(".emoji-menu-content [data-emoji='#{emoji}']") + + emojiIcon = $ ".emoji-menu-content [data-emoji='#{emoji}']" if emojiIcon.length > 0 - unicodeName = emojiIcon.data('unicode-name') + unicodeName = emojiIcon.data 'unicode-name' else # Find by alias - unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name') + unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data 'unicode-name' - "emoji-#{unicodeName}" + return "emoji-#{unicodeName}" - postEmoji: (emoji, callback) -> - $.post @postEmojiUrl, { note: { - note: ":#{emoji}:" - noteable_type: @noteableType - noteable_id: @noteableId - }},(data) -> - if data.ok - callback.call() - findEmojiIcon: (emoji) -> - $(".awards > .js-emoji-btn [data-emoji='#{emoji}']") + postEmoji: (awardUrl, emoji, callback) -> + + $.post awardUrl, { name: emoji }, (data) -> + callback() if data.ok + + + findEmojiIcon: (votesBlock, emoji) -> + + return votesBlock.find ".js-emoji-btn [data-emoji='#{emoji}']" + scrollToAwards: -> - $('body, html').animate({ - scrollTop: $('.awards').offset().top - 80 - }, 200) + + options = scrollTop: $('.awards').offset().top - 110 + $('body, html').animate options, 200 + + + normilizeEmojiName: (emoji) -> return @aliases[emoji] or emoji + addEmojiToFrequentlyUsedList: (emoji) -> + frequentlyUsedEmojis = @getFrequentlyUsedEmojis() - frequentlyUsedEmojis.push(emoji) - $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }) + frequentlyUsedEmojis.push emoji + $.cookie 'frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 } + getFrequentlyUsedEmojis: -> - frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',') - _.compact(_.uniq(frequentlyUsedEmojis)) + + frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') or '').split(',') + return _.compact _.uniq frequentlyUsedEmojis + renderFrequentlyUsedBlock: -> - if $.cookie('frequently_used_emojis') + + if $.cookie 'frequently_used_emojis' frequentlyUsedEmojis = @getFrequentlyUsedEmojis() - ul = $('<ul>') + ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>") for emoji in frequentlyUsedEmojis - do (emoji) -> - $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul) + $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul) $('input.emoji-search').after(ul).after($('<h5>').text('Frequently used')) + @frequentEmojiBlockRendered = true + + setupSearch: -> - $('input.emoji-search').keyup (ev) => + + $('input.emoji-search').on 'keyup', (ev) => term = $(ev.target).val() # Clean previous search results @@ -204,12 +357,14 @@ class @AwardsHandler if term # Generate a search result block h5 = $('<h5>').text('Search results').addClass('emoji-search') - foundEmojis = @searchEmojis(term).show() - ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis) + found_emojis = @searchEmojis(term).show() + ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis) $('.emoji-menu-content ul, .emoji-menu-content h5').hide() $('.emoji-menu-content').append(h5).append(ul) else $('.emoji-menu-content').children().show() - searchEmojis: (term)-> - $(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone() + + searchEmojis: (term) -> + + $(".emoji-menu-list:not(.frequent-emojis) [data-emoji*='#{term}']").closest('li').clone() diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee new file mode 100644 index 00000000000..cc8a497d081 --- /dev/null +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee @@ -0,0 +1,58 @@ +class @BlobGitignoreSelector + constructor: (opts) -> + { + @dropdown + @editor + @$wrapper = @dropdown.closest('.gitignore-selector') + @$filenameInput = $('#file_name') + @data = @dropdown.data('filenames') + } = opts + + @dropdown.glDropdown( + data: @data, + filterable: true, + selectable: true, + search: + fields: ['name'] + clicked: @onClick + text: (gitignore) -> + gitignore.name + ) + + @toggleGitignoreSelector() + @bindEvents() + + bindEvents: -> + @$filenameInput + .on 'keyup blur', (e) => + @toggleGitignoreSelector() + + toggleGitignoreSelector: -> + filename = @$filenameInput.val() or $('.editor-file-name').text().trim() + @$wrapper.toggleClass 'hidden', filename isnt '.gitignore' + + onClick: (item, el, e) => + e.preventDefault() + @requestIgnoreFile(item.name) + + requestIgnoreFile: (name) -> + Api.gitignoreText name, @requestIgnoreFileSuccess.bind(@) + + requestIgnoreFileSuccess: (gitignore) -> + @editor.setValue(gitignore.content, 1) + @editor.focus() + +class @BlobGitignoreSelectors + constructor: (opts) -> + { + @$dropdowns = $('.js-gitignore-selector') + @editor + } = opts + + @$dropdowns.each (i, dropdown) => + $dropdown = $(dropdown) + + new BlobGitignoreSelector( + dropdown: $dropdown, + editor: @editor + ) diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee index eea9aa972ee..79141e768b8 100644 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -13,6 +13,7 @@ class @EditBlob @initModePanesAndLinks() new BlobLicenseSelector(@editor) + new BlobGitignoreSelectors(editor: @editor) initModePanesAndLinks: -> @$editModePanes = $(".js-edit-mode-pane") diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee deleted file mode 100644 index d80e0e716ce..00000000000 --- a/app/assets/javascripts/calendar.js.coffee +++ /dev/null @@ -1,34 +0,0 @@ -class @Calendar - constructor: (timestamps, starting_year, starting_month, calendar_activities_path) -> - cal = new CalHeatMap() - cal.init - itemName: ["contribution"] - data: timestamps - start: new Date(starting_year, starting_month) - domainLabelFormat: "%b" - id: "cal-heatmap" - domain: "month" - subDomain: "day" - range: 12 - tooltip: true - label: - position: "top" - legend: [ - 0 - 10 - 20 - 30 - ] - legendCellPadding: 3 - cellSize: $('.user-calendar').width() / 73 - onClick: (date, count) -> - formated_date = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate() - $.ajax - url: calendar_activities_path - data: - date: formated_date - cache: false - dataType: "html" - success: (data) -> - $(".user-calendar-activities").html data - diff --git a/app/assets/javascripts/ci/application.js.coffee b/app/assets/javascripts/ci/application.js.coffee index 05aa0f366bb..ca24c1d759f 100644 --- a/app/assets/javascripts/ci/application.js.coffee +++ b/app/assets/javascripts/ci/application.js.coffee @@ -1,34 +1,6 @@ -# This is a manifest file that'll be compiled into application.js, which will include all the files -# listed below. -# -# Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -# or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. -# -# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -# the compiled file. -# -# WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD -# GO AFTER THE REQUIRES BELOW. -# #= require pager #= require jquery_nested_form #= require_tree . -# -$(document).on 'click', '.edit-runner-link', (event) -> - event.preventDefault() - - descr = $(this).closest('.runner-description').first() - descr.addClass('hide') - form = descr.next('.runner-description-form') - descrInput = form.find('input.description') - originalValue = descrInput.val() - form.removeClass('hide') - form.find('.cancel').on 'click', (event) -> - event.preventDefault() - - form.addClass('hide') - descrInput.val(originalValue) - descr.removeClass('hide') $(document).on 'click', '.assign-all-runner', -> $(this).replaceWith('<i class="fa fa-refresh fa-spin"></i> Assign in progress..') diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee index 7afe8bf79e2..f763ba96e33 100644 --- a/app/assets/javascripts/ci/build.coffee +++ b/app/assets/javascripts/ci/build.coffee @@ -1,16 +1,31 @@ -class CiBuild +class @CiBuild @interval: null + @state: null - constructor: (build_url, build_status) -> + constructor: (@build_url, @build_status, @state) -> clearInterval(CiBuild.interval) - @initScrollButtonAffix() + # Init breakpoint checker + @bp = Breakpoints.get() + @hideSidebar() + $('.js-build-sidebar').niceScroll() + $(document) + .off 'click', '.js-sidebar-build-toggle' + .on 'click', '.js-sidebar-build-toggle', @toggleSidebar - if build_status == "running" || build_status == "pending" + $(window) + .off 'resize.build' + .on 'resize.build', @hideSidebar + + if $('#build-trace').length + @getInitialBuildTrace() + @initScrollButtonAffix() + + if @build_status is "running" or @build_status is "pending" # # Bind autoscroll button to follow build output # - $("#autoscroll-button").bind "click", -> + $('#autoscroll-button').on 'click', -> state = $(this).data("state") if "enabled" is state $(this).data "state", "disabled" @@ -24,19 +39,37 @@ class CiBuild # Only valid for runnig build when output changes during time # CiBuild.interval = setInterval => - if window.location.href.split("#").first() is build_url - $.ajax - url: build_url - dataType: "json" - success: (build) => - if build.status == "running" - $('#build-trace code').html build.trace_html - $('#build-trace code').append '<i class="fa fa-refresh fa-spin"/>' - @checkAutoscroll() - else if build.status != build_status - Turbolinks.visit build_url + if window.location.href.split("#").first() is @build_url + @getBuildTrace() , 4000 + getInitialBuildTrace: -> + $.ajax + url: @build_url + dataType: 'json' + success: (build_data) -> + $('.js-build-output').html build_data.trace_html + + if build_data.status is 'success' or build_data.status is 'failed' + $('.js-build-refresh').remove() + + getBuildTrace: -> + $.ajax + url: "#{@build_url}/trace.json?state=#{encodeURIComponent(@state)}" + dataType: "json" + success: (log) => + if log.state + @state = log.state + + if log.status is "running" + if log.append + $('.js-build-output').append log.html + else + $('.js-build-output').html log.html + @checkAutoscroll() + else if log.status isnt @build_status + Turbolinks.visit @build_url + checkAutoscroll: -> $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state") @@ -51,4 +84,22 @@ class CiBuild $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top) ) -@CiBuild = CiBuild + shouldHideSidebar: -> + bootstrapBreakpoint = @bp.getBreakpointSize() + + bootstrapBreakpoint is 'xs' or bootstrapBreakpoint is 'sm' + + toggleSidebar: => + if @shouldHideSidebar() + $('.js-build-sidebar') + .toggleClass 'right-sidebar-expanded right-sidebar-collapsed' + + hideSidebar: => + if @shouldHideSidebar() + $('.js-build-sidebar') + .removeClass 'right-sidebar-expanded' + .addClass 'right-sidebar-collapsed' + else + $('.js-build-sidebar') + .removeClass 'right-sidebar-collapsed' + .addClass 'right-sidebar-expanded' diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index f91aa3c5ad7..29ac0f70b30 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -16,8 +16,8 @@ class Dispatcher shortcut_handler = null switch page when 'projects:issues:index' - Issues.init() Issuable.init() + new IssuableBulkActions() shortcut_handler = new ShortcutsNavigation() when 'projects:issues:show' new Issue() @@ -98,6 +98,8 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() when 'projects:labels:new', 'projects:labels:edit' new Labels() + when 'projects:labels:index' + new LabelManager() if $('.prioritized-labels').length when 'projects:network:show' # Ensure we don't create a particular shortcut handler here. This is # already created, where the network graph is created. @@ -119,7 +121,7 @@ class Dispatcher new UsersSelect() when 'projects' new NamespaceSelect() - when 'dashboard' + when 'dashboard', 'root' shortcut_handler = new ShortcutsDashboardNavigation() when 'profiles' new Profile() diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee index a4304786cbb..3d009a96d05 100644 --- a/app/assets/javascripts/due_date_select.js.coffee +++ b/app/assets/javascripts/due_date_select.js.coffee @@ -11,6 +11,7 @@ class @DueDateSelect $block = $dropdown.closest('.block') $selectbox = $dropdown.closest('.selectbox') $value = $block.find('.value') + $valueContent = $block.find('.value-content') $sidebarValue = $('.js-due-date-sidebar-value', $block) fieldName = $dropdown.data('field-name') @@ -20,14 +21,18 @@ class @DueDateSelect $dropdown.glDropdown( hidden: -> $selectbox.hide() - $value.removeAttr('style') + $value.css('display', '') ) - addDueDate = -> + addDueDate = (isDropdown) -> # Create the post date value = $("input[name='#{fieldName}']").val() - date = new Date value.replace(new RegExp('-', 'g'), ',') - mediumDate = $.datepicker.formatDate 'M d, yy', date + + if value isnt '' + date = new Date value.replace(new RegExp('-', 'g'), ',') + mediumDate = $.datepicker.formatDate 'M d, yy', date + else + mediumDate = 'None' data = {} data[abilityName] = {} @@ -37,25 +42,38 @@ class @DueDateSelect type: 'PUT' url: issueUpdateURL data: data + dataType: 'json' beforeSend: -> $loading.fadeIn() - $dropdown.trigger('loading.gl.dropdown') - $selectbox.hide() - $value.removeAttr('style') + if isDropdown + $dropdown.trigger('loading.gl.dropdown') + $selectbox.hide() + $value.css('display', '') - $value.html(mediumDate) + $valueContent.html(mediumDate) $sidebarValue.html(mediumDate) + + if value isnt '' + $('.js-remove-due-date-holder').removeClass 'hidden' + else + $('.js-remove-due-date-holder').addClass 'hidden' ).done (data) -> - $dropdown.trigger('loaded.gl.dropdown') - $dropdown.dropdown('toggle') + if isDropdown + $dropdown.trigger('loaded.gl.dropdown') + $dropdown.dropdown('toggle') $loading.fadeOut() + $block.on 'click', '.js-remove-due-date', (e) -> + e.preventDefault() + $("input[name='#{fieldName}']").val '' + addDueDate(false) + $datePicker.datepicker( dateFormat: 'yy-mm-dd', defaultDate: $("input[name='#{fieldName}']").val() altField: "input[name='#{fieldName}']" onSelect: -> - addDueDate() + addDueDate(true) ) $(document) diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee index 5de012e409f..4f73d215b85 100644 --- a/app/assets/javascripts/flash.js.coffee +++ b/app/assets/javascripts/flash.js.coffee @@ -1,5 +1,5 @@ class @Flash - constructor: (message, type)-> + constructor: (message, type = 'alert')-> @flash = $(".flash-container") @flash.html("") diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee index 61e3f811e73..76c3083232b 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.coffee +++ b/app/assets/javascripts/gfm_auto_complete.js.coffee @@ -3,6 +3,7 @@ window.GitLab ?= {} GitLab.GfmAutoComplete = dataLoading: false + dataLoaded: false dataSource: '' @@ -18,6 +19,28 @@ GitLab.GfmAutoComplete = Issues: template: '<li><small>${id}</small> ${title}</li>' + # Milestones + Milestones: + template: '<li>${title}</li>' + + Loading: + template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>' + + DefaultOptions: + sorter: (query, items, searchKey) -> + return items if items[0].name? and items[0].name is 'loading' + + $.fn.atwho.default.callbacks.sorter(query, items, searchKey) + filter: (query, data, searchKey) -> + return data if data[0] is 'loading' + + $.fn.atwho.default.callbacks.filter(query, data, searchKey) + beforeInsert: (value) -> + if not GitLab.GfmAutoComplete.dataLoaded + @at + else + value + # Add GFM auto-completion to all input fields, that accept GFM input. setup: (wrap) -> @input = $('.js-gfm-input') @@ -49,18 +72,37 @@ GitLab.GfmAutoComplete = # Emoji @input.atwho at: ':' - displayTpl: @Emoji.template + displayTpl: (value) => + if value.path? + @Emoji.template + else + @Loading.template insertTpl: ':${name}:' + data: ['loading'] + callbacks: + sorter: @DefaultOptions.sorter + filter: @DefaultOptions.filter + beforeInsert: @DefaultOptions.beforeInsert # Team Members @input.atwho at: '@' - displayTpl: @Members.template + displayTpl: (value) => + if value.username? + @Members.template + else + @Loading.template insertTpl: '${atwho-at}${username}' searchKey: 'search' + data: ['loading'] callbacks: + sorter: @DefaultOptions.sorter + filter: @DefaultOptions.filter + beforeInsert: @DefaultOptions.beforeInsert beforeSave: (members) -> $.map members, (m) -> + return m if not m.username? + title = m.name title += " (#{m.count})" if m.count @@ -72,24 +114,64 @@ GitLab.GfmAutoComplete = at: '#' alias: 'issues' searchKey: 'search' - displayTpl: @Issues.template + displayTpl: (value) => + if value.title? + @Issues.template + else + @Loading.template + data: ['loading'] insertTpl: '${atwho-at}${id}' callbacks: + sorter: @DefaultOptions.sorter + filter: @DefaultOptions.filter + beforeInsert: @DefaultOptions.beforeInsert beforeSave: (issues) -> $.map issues, (i) -> + return i if not i.title? + id: i.iid title: sanitize(i.title) search: "#{i.iid} #{i.title}" @input.atwho + at: '%' + alias: 'milestones' + searchKey: 'search' + displayTpl: (value) => + if value.title? + @Milestones.template + else + @Loading.template + insertTpl: '${atwho-at}"${title}"' + data: ['loading'] + callbacks: + beforeSave: (milestones) -> + $.map milestones, (m) -> + return m if not m.title? + + id: m.iid + title: sanitize(m.title) + search: "#{m.title}" + + @input.atwho at: '!' alias: 'mergerequests' searchKey: 'search' - displayTpl: @Issues.template + displayTpl: (value) => + if value.title? + @Issues.template + else + @Loading.template + data: ['loading'] insertTpl: '${atwho-at}${id}' callbacks: + sorter: @DefaultOptions.sorter + filter: @DefaultOptions.filter + beforeInsert: @DefaultOptions.beforeInsert beforeSave: (merges) -> $.map merges, (m) -> + return m if not m.title? + id: m.iid title: sanitize(m.title) search: "#{m.iid} #{m.title}" @@ -101,11 +183,19 @@ GitLab.GfmAutoComplete = $.getJSON(dataSource) loadData: (data) -> + @dataLoaded = true + # load members @input.atwho 'load', '@', data.members # load issues @input.atwho 'load', 'issues', data.issues + # load milestones + @input.atwho 'load', 'milestones', data.milestones # load merge requests @input.atwho 'load', 'mergerequests', data.mergerequests # load emojis @input.atwho 'load', ':', data.emojis + + # This trigger at.js again + # otherwise we would be stuck with loading until the user types + $(':focus').trigger('keyup') diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 1d1bfeb2e77..b49bd4565a7 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -11,6 +11,8 @@ class GitLabDropdownFilter $inputContainer = @input.parent() $clearButton = $inputContainer.find('.js-dropdown-input-clear') + @indeterminateIds = [] + # Clear click $clearButton.on 'click', (e) => e.preventDefault() @@ -35,20 +37,20 @@ class GitLabDropdownFilter if keyCode is 13 return false - clearTimeout timeout - timeout = setTimeout => - blur_field = @shouldBlur keyCode - search_text = @input.val() + # Only filter asynchronously only if option remote is set + if @options.remote + clearTimeout timeout + timeout = setTimeout => + blur_field = @shouldBlur keyCode - if blur_field and @filterInputBlur - @input.blur() + if blur_field and @filterInputBlur + @input.blur() - if @options.remote - @options.query search_text, (data) => + @options.query @input.val(), (data) => @options.callback(data) - else - @filter search_text - , 250 + , 250 + else + @filter @input.val() shouldBlur: (keyCode) -> return BLUR_KEYCODES.indexOf(keyCode) >= 0 @@ -60,9 +62,36 @@ class GitLabDropdownFilter results = data if search_text isnt '' - results = fuzzaldrinPlus.filter(data, search_text, - key: @options.keys - ) + # When data is an array of objects therefore [object Array] e.g. + # [ + # { prop: 'foo' }, + # { prop: 'baz' } + # ] + if _.isArray(data) + results = fuzzaldrinPlus.filter(data, search_text, + key: @options.keys + ) + else + # If data is grouped therefore an [object Object]. e.g. + # { + # groupName1: [ + # { prop: 'foo' }, + # { prop: 'baz' } + # ], + # groupName2: [ + # { prop: 'abc' }, + # { prop: 'def' } + # ] + # } + if gl.utils.isObject data + results = {} + for key, group of data + tmp = fuzzaldrinPlus.filter(group, search_text, + key: @options.keys + ) + + if tmp.length + results[key] = tmp.map (item) -> item @options.callback results else @@ -115,6 +144,7 @@ class GitLabDropdown LOADING_CLASS = "is-loading" PAGE_TWO_CLASS = "is-page-two" ACTIVE_CLASS = "is-active" + INDETERMINATE_CLASS = "is-indeterminate" currentIndex = -1 FILTER_INPUT = '.dropdown-input .dropdown-input-field' @@ -141,8 +171,9 @@ class GitLabDropdown searchFields = if @options.search then @options.search.fields else []; if @options.data - # If data is an array - if _.isArray @options.data + # If we provided data + # data could be an array of objects or a group of arrays + if _.isObject(@options.data) and not _.isFunction(@options.data) @fullData = @options.data @parseData @options.data else @@ -154,9 +185,6 @@ class GitLabDropdown @fullData = data @parseData @fullData - - if @options.filterable - @filterInput.trigger 'keyup' } # Init filterable @@ -183,6 +211,7 @@ class GitLabDropdown @dropdown.on "shown.bs.dropdown", @opened @dropdown.on "hidden.bs.dropdown", @hidden + $(@el).on "update.label", @updateLabel @dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate @dropdown.on 'keyup', (e) => if e.which is 27 # Escape key @@ -230,19 +259,33 @@ class GitLabDropdown parseData: (data) -> @renderedData = data - # Render each row - html = $.map data, (obj) => - return @renderItem(obj) - if @options.filterable and data.length is 0 # render no matching results html = [@noResults()] + else + # Handle array groups + if gl.utils.isObject data + html = [] + for name, groupData of data + # Add header for each group + html.push(@renderItem(header: name, name)) + + @renderData(groupData, name) + .map (item) -> + html.push item + else + # Render each row + html = @renderData(data) # Render the full menu full_html = @renderMenu(html.join("")) @appendMenu(full_html) + renderData: (data, group = false) -> + data.map (obj, index) => + return @renderItem(obj, group, index) + shouldPropagate: (e) => if @options.multiSelect $target = $(e.target) @@ -256,6 +299,13 @@ class GitLabDropdown opened: => @addArrowKeyEvent() + if @options.setIndeterminateIds + @options.setIndeterminateIds.call(@) + + # Makes indeterminate items effective + if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') + @parseData @fullData + contentHtml = $('.dropdown-content', @dropdown).html() if @remote && contentHtml is "" @remote.execute() @@ -267,12 +317,18 @@ class GitLabDropdown hidden: (e) => @removeArrayKeyEvent() + + $input = @dropdown.find(".dropdown-input-field") + if @options.filterable - @dropdown - .find(".dropdown-input-field") + $input .blur() .val("") - .trigger("keyup") + + # Triggering 'keyup' will re-render the dropdown which is not always required + # specially if we want to keep the state of the dropdown needed for bulk-assignment + if not @options.persistWhenHide + $input.trigger("keyup") if @dropdown.find(".dropdown-toggle-page").length $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS @@ -299,11 +355,10 @@ class GitLabDropdown selector = '.dropdown-content' if @dropdown.find(".dropdown-toggle-page").length selector = ".dropdown-page-one .dropdown-content" - $(selector, @dropdown).html html # Render the row - renderItem: (data) -> + renderItem: (data, group = false, index = false) -> html = "" # Divider @@ -317,7 +372,7 @@ class GitLabDropdown if @options.renderRow # Call the render function - html = @options.renderRow(data) + html = @options.renderRow.call(@options, data, @) else if not selected value = if @options.id then @options.id(data) else data.id @@ -346,8 +401,13 @@ class GitLabDropdown if @highlight text = @highlightTextMatches(text, @filterInput.val()) + if group + groupAttrs = "data-group='#{group}' data-index='#{index}'" + else + groupAttrs = '' + html = "<li> - <a href='#{url}' class='#{cssClass}'> + <a href='#{url}' #{groupAttrs} class='#{cssClass}'> #{text} </a> </li>" @@ -377,9 +437,15 @@ class GitLabDropdown rowClicked: (el) -> fieldName = @options.fieldName - selectedIndex = el.parent().index() if @renderedData - selectedObject = @renderedData[selectedIndex] + groupName = el.data('group') + if groupName + selectedIndex = el.data('index') + selectedObject = @renderedData[groupName][selectedIndex] + else + selectedIndex = el.closest('li').index() + selectedObject = @renderedData[selectedIndex] + value = if @options.id then @options.id(selectedObject, el) else selectedObject.id field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']") if el.hasClass(ACTIVE_CLASS) @@ -388,9 +454,20 @@ class GitLabDropdown # Toggle the dropdown label if @options.toggleLabel - $(@el).find(".dropdown-toggle-text").text @options.toggleLabel + @updateLabel() else selectedObject + else if el.hasClass(INDETERMINATE_CLASS) + el.addClass ACTIVE_CLASS + el.removeClass INDETERMINATE_CLASS + + if not value? + field.remove() + + if not field.length and fieldName + @addInput(fieldName, value) + + return selectedObject else if not @options.multiSelect or el.hasClass('dropdown-clear-active') @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS @@ -404,34 +481,45 @@ class GitLabDropdown # Toggle the dropdown label if @options.toggleLabel - $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el) + @updateLabel(selectedObject, el) if value? if !field.length and fieldName - # Create hidden input for form - input = "<input type='hidden' name='#{fieldName}' value='#{value}' />" - if @options.inputId? - input = $(input) - .attr('id', @options.inputId) - @dropdown.before input + @addInput(fieldName, value) else field.val value return selectedObject - selectRowAtIndex: (index) -> - selector = ".dropdown-content li:not(.divider):eq(#{index}) a" + addInput: (fieldName, value)-> + # Create hidden input for form + $input = $('<input>').attr('type', 'hidden') + .attr('name', fieldName) + .val(value) + + if @options.inputId? + $input.attr('id', @options.inputId) + + @dropdown.before $input + + selectRowAtIndex: (e, index) -> + selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(#{index}) a" if @dropdown.find(".dropdown-toggle-page").length selector = ".dropdown-page-one #{selector}" # simulate a click on the first link - $(selector, @dropdown).trigger "click" + $el = $(selector, @dropdown) + + if $el.length + e.preventDefault() + e.stopImmediatePropagation() + $(selector, @dropdown)[0].click() addArrowKeyEvent: -> ARROW_KEY_CODES = [38, 40] $input = @dropdown.find(".dropdown-input-field") - selector = '.dropdown-content li:not(.divider)' + selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator)' if @dropdown.find(".dropdown-toggle-page").length selector = ".dropdown-page-one #{selector}" @@ -459,8 +547,8 @@ class GitLabDropdown return false - if currentKeyCode is 13 - @selectRowAtIndex currentIndex + if currentKeyCode is 13 and currentIndex isnt -1 + @selectRowAtIndex e, currentIndex removeArrayKeyEvent: -> $('body').off 'keydown' @@ -492,6 +580,9 @@ class GitLabDropdown # Scroll the dropdown content up $dropdownContent.scrollTop(listItemTop - dropdownContentTop) + updateLabel: (selected = null, el = null) => + $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selected, el) + $.fn.glDropdown = (opts) -> return @.each -> if (!$.data @, 'glDropdown') diff --git a/app/assets/javascripts/graphs/application.js.coffee b/app/assets/javascripts/graphs/application.js.coffee new file mode 100644 index 00000000000..91f81a5d249 --- /dev/null +++ b/app/assets/javascripts/graphs/application.js.coffee @@ -0,0 +1,8 @@ +# This is a manifest file that'll be compiled into including all the files listed below. +# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +# be included in the compiled file accessible from http://example.com/assets/application.js +# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +# the compiled file. +# +#= require Chart +#= require_tree . diff --git a/app/assets/javascripts/stat_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph.js.coffee index f36c71fd25e..f36c71fd25e 100644 --- a/app/assets/javascripts/stat_graph.js.coffee +++ b/app/assets/javascripts/graphs/stat_graph.js.coffee diff --git a/app/assets/javascripts/stat_graph_contributors.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee index 3be14cb43dd..1d9fae7cf79 100644 --- a/app/assets/javascripts/stat_graph_contributors.js.coffee +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee @@ -1,5 +1,4 @@ #= require d3 -#= require stat_graph_contributors_util class @ContributorsStatGraph init: (log) -> diff --git a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee index b7a0e073766..584d281a510 100644 --- a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee @@ -1,6 +1,4 @@ #= require d3 -#= require jquery -#= require underscore class @ContributorsGraph MARGIN: diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee index 31617c88b4a..31617c88b4a 100644 --- a/app/assets/javascripts/stat_graph_contributors_util.js.coffee +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee index afffed63ac5..c2447120033 100644 --- a/app/assets/javascripts/issuable.js.coffee +++ b/app/assets/javascripts/issuable.js.coffee @@ -1,13 +1,23 @@ +issuable_created = false @Issuable = init: -> - Issuable.initTemplates() - Issuable.initSearch() + unless issuable_created + issuable_created = true + Issuable.initTemplates() + Issuable.initSearch() + Issuable.initChecks() + Issuable.initLabelFilterRemove() initTemplates: -> Issuable.labelRow = _.template( '<% _.each(labels, function(label){ %> - <span class="label-row"> - <a href="#"><span class="label color-label has-tooltip" style="background-color: <%= label.color %>; color: <%= label.text_color %>" title="<%= _.escape(label.description) %>" data-container="body"><%= _.escape(label.title) %></span></a> + <span class="label-row btn-group" role="group" aria-label="<%= _.escape(label.title) %>" style="color: <%= label.text_color %>;"> + <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%= label.color %>;" title="<%= _.escape(label.description) %>" data-container="body"> + <%= _.escape(label.title) %> + </a> + <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%= label.color %>;" data-label="<%= _.escape(label.title) %>"> + <i class="fa fa-times"></i> + </button> </span> <% }); %>' ) @@ -19,9 +29,33 @@ .on 'keyup', -> clearTimeout(@timer) @timer = setTimeout( -> - Issuable.filterResults $('#issue_search_form') + $search = $('#issue_search') + $form = $('.js-filter-form') + $input = $("input[name='#{$search.attr('name')}']", $form) + + if $input.length is 0 + $form.append "<input type='hidden' name='#{$search.attr('name')}' value='#{_.escape($search.val())}'/>" + else + $input.val $search.val() + + Issuable.filterResults $form , 500) + initLabelFilterRemove: -> + $(document) + .off 'click', '.js-label-filter-remove' + .on 'click', '.js-label-filter-remove', (e) -> + $button = $(@) + + # Remove the label input box + $('input[name="label_name[]"]') + .filter -> @value is $button.data('label') + .remove() + + # Submit the form to get new data + Issuable.filterResults $('.filter-form') + $('.js-label-select').trigger('update.label') + toggleLabelFilters: -> $filteredLabels = $('.filtered-labels') if $filteredLabels.find('.label-row').length > 0 @@ -59,15 +93,22 @@ dataType: "json" reload: -> - if Issues.created - Issues.initChecks() + if Issuable.created + Issuable.initChecks() $('#filter_issue_search').val($('#issue_search').val()) + initChecks: -> + $('.check_all_issues').on 'click', -> + $('.selected_issue').prop('checked', @checked) + Issuable.checkChanged() + + $('.selected_issue').on 'change', Issuable.checkChanged + updateStateFilters: -> - stateFilters = $('.issues-state-filters') + stateFilters = $('.issues-state-filters, .dropdown-menu-sort') newParams = {} - paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search'] + paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search', 'issue_search'] for paramKey in paramKeys newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or '' @@ -82,3 +123,17 @@ else newUrl = gl.utils.mergeUrlParams(newParams, initialUrl) $(this).attr 'href', newUrl + + checkChanged: -> + checked_issues = $('.selected_issue:checked') + if checked_issues.length > 0 + ids = $.map checked_issues, (value) -> + $(value).data('id') + + $('#update_issues_ids').val ids + $('.issues-other-filters').hide() + $('.issues_bulk_update').show() + else + $('#update_issues_ids').val [] + $('.issues_bulk_update').hide() + $('.issues-other-filters').show() diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee index 7a788f761b7..898506fde32 100644 --- a/app/assets/javascripts/issuable_form.js.coffee +++ b/app/assets/javascripts/issuable_form.js.coffee @@ -19,6 +19,16 @@ class @IssuableForm @form.on "click", ".btn-cancel", @resetAutosave @initWip() + @initMoveDropdown() + + $issuableDueDate = $('#issuable-due-date') + + if $issuableDueDate.length + $('.datepicker').datepicker( + dateFormat: 'yy-mm-dd', + onSelect: (dateText, inst) -> + $issuableDueDate.val dateText + ).datepicker 'setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val()) initAutosave: -> new Autosave @titleField, [ @@ -80,3 +90,19 @@ class @IssuableForm addWip: -> @titleField.val "WIP: #{@titleField.val()}" + + initMoveDropdown: -> + $moveDropdown = $('.js-move-dropdown') + + if $moveDropdown.length + $('.js-move-dropdown').select2 + ajax: + url: $moveDropdown.data('projects-url') + results: (data) -> + return { + results: data + } + formatResult: (project) -> + project.name_with_namespace + formatSelection: (project) -> + project.name_with_namespace diff --git a/app/assets/javascripts/issues-bulk-assignment.js.coffee b/app/assets/javascripts/issues-bulk-assignment.js.coffee new file mode 100644 index 00000000000..16d023dd391 --- /dev/null +++ b/app/assets/javascripts/issues-bulk-assignment.js.coffee @@ -0,0 +1,109 @@ +class @IssuableBulkActions + constructor: (opts = {}) -> + # Set defaults + { + @container = $('.content') + @form = @getElement('.bulk-update') + @issues = @getElement('.issues-list .issue') + } = opts + + @bindEvents() + + getElement: (selector) -> + @container.find selector + + bindEvents: -> + @form.off('submit').on('submit', @onFormSubmit.bind(@)) + + onFormSubmit: (e) -> + e.preventDefault() + @submit() + + submit: -> + _this = @ + + xhr = $.ajax + url: @form.attr 'action' + method: @form.attr 'method' + dataType: 'JSON', + data: @getFormDataAsObject() + + xhr.done (response, status, xhr) -> + location.reload() + + xhr.fail -> + new Flash("Issue update failed") + + xhr.always @onFormSubmitAlways.bind(@) + + onFormSubmitAlways: -> + @form.find('[type="submit"]').enable() + + getSelectedIssues: -> + @issues.has('.selected_issue:checked') + + getLabelsFromSelection: -> + labels = [] + + @getSelectedIssues().map -> + _labels = $(@).data('labels') + if _labels + _labels.map (labelId) -> + labels.push(labelId) if labels.indexOf(labelId) is -1 + + labels + + ###* + * Will return only labels that were marked previously and the user has unmarked + * @return {Array} Label IDs + ### + getUnmarkedIndeterminedLabels: -> + result = [] + labelsToKeep = [] + + for el in @getElement('.labels-filter .is-indeterminate') + labelsToKeep.push $(el).data('labelId') + + for id in @getLabelsFromSelection() + # Only the ones that we are not going to keep + result.push(id) if labelsToKeep.indexOf(id) is -1 + + result + + ###* + * Simple form serialization, it will return just what we need + * Returns key/value pairs from form data + ### + getFormDataAsObject: -> + formData = + update: + state_event : @form.find('input[name="update[state_event]"]').val() + assignee_id : @form.find('input[name="update[assignee_id]"]').val() + milestone_id : @form.find('input[name="update[milestone_id]"]').val() + issues_ids : @form.find('input[name="update[issues_ids]"]').val() + add_label_ids : [] + remove_label_ids : [] + + @getLabelsToApply().map (id) -> + formData.update.add_label_ids.push id + + @getLabelsToRemove().map (id) -> + formData.update.remove_label_ids.push id + + formData + + getLabelsToApply: -> + labelIds = [] + $labels = @form.find('.labels-filter input[name="update[label_ids][]"]') + + $labels.each (k, label) -> + labelIds.push $(label).val() if label + + labelIds + + ###* + * Just an alias of @getUnmarkedIndeterminedLabels + * @return {Array} Array of labels + ### + getLabelsToRemove: -> + @getUnmarkedIndeterminedLabels() diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee deleted file mode 100644 index 3330e6c68ad..00000000000 --- a/app/assets/javascripts/issues.js.coffee +++ /dev/null @@ -1,38 +0,0 @@ -@Issues = - init: -> - Issues.created = true - Issues.initChecks() - - $("body").on "ajax:success", ".close_issue, .reopen_issue", -> - t = $(this) - totalIssues = undefined - reopen = t.hasClass("reopen_issue") - $(".issue_counter").each -> - issue = $(this) - totalIssues = parseInt($(this).html(), 10) - if reopen and issue.closest(".main_menu").length - $(this).html totalIssues + 1 - else - $(this).html totalIssues - 1 - - initChecks: -> - $(".check_all_issues").click -> - $(".selected_issue").prop("checked", @checked) - Issues.checkChanged() - - $(".selected_issue").bind "change", Issues.checkChanged - - checkChanged: -> - checked_issues = $(".selected_issue:checked") - if checked_issues.length > 0 - ids = [] - $.each checked_issues, (index, value) -> - ids.push $(value).attr("data-id") - - $("#update_issues_ids").val ids - $(".issues-other-filters").hide() - $(".issues_bulk_update").show() - else - $("#update_issues_ids").val [] - $(".issues_bulk_update").hide() - $(".issues-other-filters").show() diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 995fd768603..9ca88f1226e 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -1,5 +1,7 @@ class @LabelsSelect constructor: -> + _this = @ + $('.js-label-select').each (i, dropdown) -> $dropdown = $(dropdown) projectId = $dropdown.data('project-id') @@ -93,8 +95,11 @@ class @LabelsSelect $newLabelCreateButton.enable() if label.message? + errors = _.map label.message, (value, key) -> + "#{key} #{value[0]}" + $newLabelError - .text label.message + .html errors.join("<br/>") .show() else $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' @@ -196,10 +201,18 @@ class @LabelsSelect callback data - renderRow: (label) -> - removesAll = label.id is 0 or not label.id? + renderRow: (label, instance) -> + $li = $('<li>') + $a = $('<a href="#">') selectedClass = [] + removesAll = label.id is 0 or not label.id? + + if $dropdown.hasClass('js-filter-bulk-update') + indeterminate = instance.indeterminateIds + if indeterminate.indexOf(label.id) isnt -1 + selectedClass.push 'is-indeterminate' + if $form.find("input[type='hidden']\ [name='#{$dropdown.data('fieldName')}']\ [value='#{this.id(label)}']").length @@ -230,17 +243,21 @@ class @LabelsSelect else colorEl = '' - "<li> - <a href='#' class='#{selectedClass.join(' ')}'> - #{colorEl} - #{_.escape(label.title)} - </a> - </li>" - filterable: true + # We need to identify which items are actually labels + if label.id + selectedClass.push('label-item') + $a.attr('data-label-id', label.id) + + $a.addClass(selectedClass.join(' ')) + .html("#{colorEl} #{_.escape(label.title)}") + + # Return generated html + $li.html($a).prop('outerHTML') + persistWhenHide: $dropdown.data('persistWhenHide') search: fields: ['title'] selectable: true - + filterable: true toggleLabel: (selected, el) -> selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active') @@ -280,10 +297,19 @@ class @LabelsSelect else if $dropdown.hasClass('js-filter-submit') $dropdown.closest('form').submit() else - saveLabelData() + if not $dropdown.hasClass 'js-filter-bulk-update' + saveLabelData() + + if $dropdown.hasClass('js-filter-bulk-update') + # If we are persisting state we need the classes + if not @options.persistWhenHide + $dropdown.parent().find('.is-active, .is-indeterminate').removeClass() multiSelect: $dropdown.hasClass 'js-multiselect' clicked: (label) -> + if $dropdown.hasClass('js-filter-bulk-update') + return + page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' isMRIndex = page is 'projects:merge_requests:index' @@ -298,4 +324,31 @@ class @LabelsSelect return else saveLabelData() + + setIndeterminateIds: -> + if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update') + @indeterminateIds = _this.getIndeterminateIds() ) + + @bindEvents() + + bindEvents: -> + $('body').on 'change', '.selected_issue', @onSelectCheckboxIssue + + onSelectCheckboxIssue: -> + return if $('.selected_issue:checked').length + + # Remove inputs + $('.issues_bulk_update .labels-filter input[type="hidden"]').remove() + + # Also restore button text + $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label') + + getIndeterminateIds: -> + label_ids = [] + + $('.selected_issue:checked').each (i, el) -> + issue_id = $(el).data('id') + label_ids.push $("#issue_#{issue_id}").data('labels') + + _.flatten(label_ids) diff --git a/app/assets/javascripts/layout_nav.js.coffee b/app/assets/javascripts/layout_nav.js.coffee new file mode 100644 index 00000000000..6adac6dac97 --- /dev/null +++ b/app/assets/javascripts/layout_nav.js.coffee @@ -0,0 +1,14 @@ +class @LayoutNav + $ -> + $('.fade-left').addClass('end-scroll') + $('.scrolling-tabs').on 'scroll', (event) -> + $this = $(this) + $el = $(event.target) + currentPosition = $this.scrollLeft() + size = bp.getBreakpointSize() + controlBtnWidth = $('.controls').width() + maxPosition = $this.get(0).scrollWidth - $this.parent().width() + maxPosition += controlBtnWidth if size isnt 'xs' and $('.nav-control').length + + $el.find('.fade-left').toggleClass('end-scroll', currentPosition is 0) + $el.find('.fade-right').toggleClass('end-scroll', currentPosition is maxPosition) diff --git a/app/assets/javascripts/lib/common_utils.js.coffee b/app/assets/javascripts/lib/common_utils.js.coffee new file mode 100644 index 00000000000..0000e99a650 --- /dev/null +++ b/app/assets/javascripts/lib/common_utils.js.coffee @@ -0,0 +1,24 @@ +((w) -> + + jQuery.timefor = (time, suffix, expiredLabel) -> + + return '' unless time + + suffix or= 'remaining' + expiredLabel or= 'Past due' + + jQuery.timeago.settings.allowFuture = yes + + { suffixFromNow } = jQuery.timeago.settings.strings + jQuery.timeago.settings.strings.suffixFromNow = suffix + + timefor = $.timeago time + + if timefor.indexOf('ago') > -1 + timefor = expiredLabel + + jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow + + return timefor + +) window diff --git a/app/assets/javascripts/lib/datetime_utility.js.coffee b/app/assets/javascripts/lib/datetime_utility.js.coffee index ad1d1c70481..948d6dbf07e 100644 --- a/app/assets/javascripts/lib/datetime_utility.js.coffee +++ b/app/assets/javascripts/lib/datetime_utility.js.coffee @@ -12,6 +12,13 @@ $el.attr('title', gl.utils.formatDate($el.attr('datetime'))) ) - $timeagoEls.timeago() if setTimeago + if setTimeago + $timeagoEls.timeago() + $timeagoEls.tooltip('destroy') + + # Recreate with custom template + $timeagoEls.tooltip( + template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' + ) ) window diff --git a/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb new file mode 100644 index 00000000000..80f9936b9c2 --- /dev/null +++ b/app/assets/javascripts/lib/emoji_aliases.js.coffee.erb @@ -0,0 +1,2 @@ +gl.emojiAliases = -> + JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>') diff --git a/app/assets/javascripts/lib/type_utility.js.coffee b/app/assets/javascripts/lib/type_utility.js.coffee new file mode 100644 index 00000000000..957f0d86b36 --- /dev/null +++ b/app/assets/javascripts/lib/type_utility.js.coffee @@ -0,0 +1,9 @@ +((w) -> + + w.gl ?= {} + w.gl.utils ?= {} + + w.gl.utils.isObject = (obj) -> + obj? and (obj.constructor is Object) + +) window diff --git a/app/assets/javascripts/lib/url_utility.js.coffee b/app/assets/javascripts/lib/url_utility.js.coffee index 6a00932c028..e8085e1c2e4 100644 --- a/app/assets/javascripts/lib/url_utility.js.coffee +++ b/app/assets/javascripts/lib/url_utility.js.coffee @@ -26,10 +26,19 @@ newUrl = decodeURIComponent(url) for paramName, paramValue of params pattern = new RegExp "\\b(#{paramName}=).*?(&|$)" - if url.search(pattern) >= 0 + if not paramValue? + newUrl = newUrl.replace pattern, '' + else if url.search(pattern) isnt -1 newUrl = newUrl.replace pattern, "$1#{paramValue}$2" else newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}" + + # Remove a trailing ampersand + lastChar = newUrl[newUrl.length - 1] + + if lastChar is '&' + newUrl = newUrl.slice 0, -1 + newUrl # removes parameter query string from url. returns the modified url diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee index d14b7139237..9fdc27a9787 100644 --- a/app/assets/javascripts/logo.js.coffee +++ b/app/assets/javascripts/logo.js.coffee @@ -47,4 +47,4 @@ $ -> # Make logo clickable as part of a workaround for Safari visited # link behaviour (See !2690). $('#logo').on 'click', -> - $('#js-shortcuts-home').get(0).click() + Turbolinks.visit('/') diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 372732d0aac..49a4727205a 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -75,6 +75,9 @@ class @MergeRequestTabs @loadDiff($target.attr('href')) if bp? and bp.getBreakpointSize() isnt 'lg' @shrinkView() + + navBarHeight = $('.navbar-gitlab').outerHeight() + $.scrollTo(".merge-request-details .merge-request-tabs", offset: -navBarHeight) else if action == 'builds' @loadBuilds($target.attr('href')) @expandView() diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index f58647988a2..779f536d9f0 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -10,6 +10,7 @@ class @MergeRequestWidget $('#modal_merge_info').modal(show: false) @firstCICheck = true @readyForCICheck = false + @cancel = false clearInterval @fetchBuildStatusInterval @clearEventListeners() @@ -21,10 +22,16 @@ class @MergeRequestWidget clearEventListeners: -> $(document).off 'page:change.merge_request' + cancelPolling: -> + @cancel = true + addEventListeners: -> + allowedPages = ['show', 'commits', 'builds', 'changes'] $(document).on 'page:change.merge_request', => - if $('body').data('page') isnt 'projects:merge_requests:show' + page = $('body').data('page').split(':').last() + if allowedPages.indexOf(page) < 0 clearInterval @fetchBuildStatusInterval + @cancelPolling() @clearEventListeners() mergeInProgress: (deleteSourceBranch = false)-> @@ -67,6 +74,7 @@ class @MergeRequestWidget $('.ci-widget-fetching').show() $.getJSON @opts.ci_status_url, (data) => + return if @cancel @readyForCICheck = true if data.status is '' @@ -106,6 +114,7 @@ class @MergeRequestWidget @firstCICheck = false showCIStatus: (state) -> + return if not state? $('.ci_widget').hide() allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"] if state in allowed_states @@ -113,7 +122,7 @@ class @MergeRequestWidget switch state when "failed", "canceled", "not_found" @setMergeButtonClass('btn-danger') - when "running", "pending" + when "running" @setMergeButtonClass('btn-warning') when "success" @setMergeButtonClass('btn-create') @@ -126,6 +135,6 @@ class @MergeRequestWidget $('.ci_widget:visible .ci-coverage').text(text) setMergeButtonClass: (css_class) -> - $('.accept_merge_request') + $('.js-merge-button,.accept-action .dropdown-toggle') .removeClass('btn-danger btn-warning btn-create') .addClass(css_class) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 345a0e447af..648e1f3bde0 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -24,11 +24,21 @@ class @MilestoneSelect if issueUpdateURL milestoneLinkTemplate = _.template( - '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= _.escape(title) %></a>' + '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"> + <span class="has-tooltip" data-container="body" title="<%= remaining %>"> + <%= _.escape(title) %> + </span> + </a>' ) milestoneLinkNoneTemplate = '<div class="light">None</div>' + collapsedSidebarLabelTemplate = _.template( + '<span class="has-tooltip" data-container="body" title="<%= remaining %>" data-placement="left"> + <%= _.escape(title) %> + </span>' + ) + $dropdown.glDropdown( data: (term, callback) -> $.ajax( @@ -83,7 +93,7 @@ class @MilestoneSelect $selectbox.hide() # display:block overrides the hide-collapse rule - $value.removeAttr('style') + $value.css('display', '') clicked: (selected) -> page = $('body').data 'page' isIssueIndex = page is 'projects:issues:index' @@ -118,12 +128,13 @@ class @MilestoneSelect $dropdown.trigger('loaded.gl.dropdown') $loading.fadeOut() $selectbox.hide() - $value.removeAttr('style') + $value.css('display', '') if data.milestone? data.milestone.namespace = _this.currentProject.namespace data.milestone.path = _this.currentProject.path + data.milestone.remaining = $.timefor data.milestone.due_date $value.html(milestoneLinkTemplate(data.milestone)) - $sidebarCollapsedValue.find('span').text(data.milestone.title) + $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)) else $value.html(milestoneLinkNoneTemplate) $sidebarCollapsedValue.find('span').text('No') diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index efb3e8e2198..ad216910c8d 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -114,9 +114,9 @@ class @Notes @refresh() , @pollingInterval - refresh: -> + refresh: => return if @refreshing is true - refreshing = true + @refreshing = true if not document.hidden and document.URL.indexOf(@noteable_url) is 0 @getContent() @@ -134,8 +134,8 @@ class @Notes @renderDiscussionNote(note) else @renderNote(note) - always: => - @refreshing = false + .always () => + @refreshing = false ### Increase @pollingInterval up to 120 seconds on every function call, @@ -162,13 +162,14 @@ class @Notes renderNote: (note) -> unless note.valid if note.award - flash = new Flash('You have already used this award emoji!', 'alert') + flash = new Flash('You have already awarded this emoji!', 'alert') flash.pinTo('.header-content') return if note.award - awardsHandler.addAwardToEmojiBar(note.note) - awardsHandler.scrollToAwards() + votesBlock = $('.js-awards-block').eq 0 + gl.awardsHandler.addAwardToEmojiBar votesBlock, note.name + gl.awardsHandler.scrollToAwards() # render note if it not present in loaded list # or skip if rendered @@ -285,6 +286,7 @@ class @Notes form.addClass "js-main-target-form" form.find("#note_line_code").remove() + form.find("#note_type").remove() ### General note form setup. @@ -328,7 +330,7 @@ class @Notes @renderDiscussionNote(note) # cleanup after successfully creating a diff/discussion note - @removeDiscussionNoteForm($("#new-discussion-note-form-#{note.discussion_id}")) + @removeDiscussionNoteForm($(xhr.target)) ### Called in response to the edit note form being submitted @@ -352,8 +354,7 @@ class @Notes Called in response to clicking the edit note link Replaces the note text with the note edit form - Adds a hidden div with the original content of the note to fill the edit note form with - if the user cancels + Adds a data attribute to the form with the original content of the note for cancellations ### showEditForm: (e, scrollTo, myLastNote) -> e.preventDefault() @@ -369,6 +370,8 @@ class @Notes done = ($noteText) -> # Neat little trick to put the cursor at the end noteTextVal = $noteText.val() + # Store the original note text in a data attribute to retrieve if a user cancels edit. + form.find('form.edit-note').data 'original-note', noteTextVal $noteText.val('').val(noteTextVal); new GLForm form @@ -391,14 +394,16 @@ class @Notes ### Called in response to clicking the edit note link - Hides edit form + Hides edit form and restores the original note text to the editor textarea. ### cancelEdit: (e) -> e.preventDefault() note = $(this).closest(".note") + form = note.find(".current-note-edit-form") note.removeClass "is-editting" - note.find(".current-note-edit-form") - .removeClass("current-note-edit-form") + form.removeClass("current-note-edit-form") + # Replace markdown textarea text with original note text. + form.find(".js-note-text").val(form.find('form.edit-note').data('original-note')) ### Called in response to deleting a note of any kind. @@ -472,6 +477,7 @@ class @Notes setupDiscussionNoteForm: (dataHolder, form) => # setup note target form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}" + form.find("#note_type").val dataHolder.data("noteType") form.find("#line_type").val dataHolder.data("lineType") form.find("#note_commit_id").val dataHolder.data("commitId") form.find("#note_line_code").val dataHolder.data("lineCode") diff --git a/app/assets/javascripts/pager.js.coffee b/app/assets/javascripts/pager.js.coffee index 0ff83b7f0c8..8049c5c30e2 100644 --- a/app/assets/javascripts/pager.js.coffee +++ b/app/assets/javascripts/pager.js.coffee @@ -1,5 +1,5 @@ @Pager = - init: (@limit = 0, preload, @disable = false) -> + init: (@limit = 0, preload, @disable = false, @callback = $.noop) -> @loading = $('.loading').first() if preload @@ -19,6 +19,7 @@ @loading.hide() success: (data) -> Pager.append(data.count, data.html) + Pager.callback() dataType: "json" append: (count, html) -> diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee index 63dee4ed5d7..e48343a19b5 100644 --- a/app/assets/javascripts/project_new.js.coffee +++ b/app/assets/javascripts/project_new.js.coffee @@ -7,12 +7,17 @@ class @ProjectNew @toggleSettingsOnclick() - toggleSettings: -> - checked = $("#project_builds_enabled").prop("checked") - if checked - $('.builds-feature').show() - else - $('.builds-feature').hide() + toggleSettings: => + @_showOrHide('#project_builds_enabled', '.builds-feature') + @_showOrHide('#project_merge_requests_enabled', '.merge-requests-feature') toggleSettingsOnclick: -> - $("#project_builds_enabled").on 'click', @toggleSettings + $('#project_builds_enabled, #project_merge_requests_enabled').on 'click', @toggleSettings + + _showOrHide: (checkElement, container) -> + $container = $(container) + + if $(checkElement).prop('checked') + $container.show() + else + $container.hide() diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee index 2d084b76cfe..c9cb0f4bb32 100644 --- a/app/assets/javascripts/right_sidebar.js.coffee +++ b/app/assets/javascripts/right_sidebar.js.coffee @@ -10,6 +10,40 @@ class @Sidebar $('.dropdown').on('loading.gl.dropdown', @sidebarDropdownLoading) $('.dropdown').on('loaded.gl.dropdown', @sidebarDropdownLoaded) + + $(document) + .off 'click', '.js-sidebar-toggle' + .on 'click', '.js-sidebar-toggle', (e, triggered) -> + e.preventDefault() + $this = $(this) + $thisIcon = $this.find 'i' + $allGutterToggleIcons = $('.js-sidebar-toggle i') + if $thisIcon.hasClass('fa-angle-double-right') + $allGutterToggleIcons + .removeClass('fa-angle-double-right') + .addClass('fa-angle-double-left') + $('aside.right-sidebar') + .removeClass('right-sidebar-expanded') + .addClass('right-sidebar-collapsed') + $('.page-with-sidebar') + .removeClass('right-sidebar-expanded') + .addClass('right-sidebar-collapsed') + else + $allGutterToggleIcons + .removeClass('fa-angle-double-left') + .addClass('fa-angle-double-right') + $('aside.right-sidebar') + .removeClass('right-sidebar-collapsed') + .addClass('right-sidebar-expanded') + $('.page-with-sidebar') + .removeClass('right-sidebar-collapsed') + .addClass('right-sidebar-expanded') + if not triggered + $.cookie("collapsed_gutter", + $('.right-sidebar') + .hasClass('right-sidebar-collapsed'), { path: '/' }) + + sidebarDropdownLoading: (e) -> $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon') img = $sidebarCollapsedIcon.find('img') @@ -76,7 +110,7 @@ class @Sidebar @triggerOpenSidebar() if not @isOpen() if action is 'hide' - @triggerOpenSidebar() is @isOpen() + @triggerOpenSidebar() if @isOpen() isOpen: -> @sidebar.is('.right-sidebar-expanded') diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 6a7b4ad1db7..5eb915a51ea 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -20,8 +20,7 @@ class @SearchAutocomplete @dropdown = @wrap.find('.dropdown') @dropdownContent = @dropdown.find('.dropdown-content') - @locationBadgeEl = @getElement('.search-location-badge') - @locationText = @getElement('.location-text') + @locationBadgeEl = @getElement('.location-badge') @scopeInputEl = @getElement('#scope') @searchInput = @getElement('.search-input') @projectInputEl = @getElement('#search_project_id') @@ -133,7 +132,7 @@ class @SearchAutocomplete scope: @scopeInputEl.val() # Location badge - _location: @locationText.text() + _location: @locationBadgeEl.text() } bindEvents: -> @@ -143,23 +142,28 @@ class @SearchAutocomplete @searchInput.on 'click', @onSearchInputClick @searchInput.on 'focus', @onSearchInputFocus @clearInput.on 'click', @onClearInputClick + @locationBadgeEl.on 'click', => + @searchInput.focus() onDocumentClick: (e) => # If clicking outside the search box # And search input is not focused # And we are not clicking inside a suggestion - if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).parents('ul').length + if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).closest('.search-form').length @onSearchInputBlur() enableAutocomplete: -> # No need to enable anything if user is not logged in return if !gon.current_user_id - _this = @ - @loadingSuggestions = false + unless @dropdown.hasClass('open') + _this = @ + @loadingSuggestions = false - @dropdown.addClass('open') - @searchInput.removeClass('disabled') + @dropdown + .addClass('open') + .trigger('shown.bs.dropdown') + @searchInput.removeClass('disabled') onSearchInputKeyDown: => # Saves last length of the entered text @@ -190,7 +194,7 @@ class @SearchAutocomplete @disableAutocomplete() else # We should display the menu only when input is not empty - @enableAutocomplete() + @enableAutocomplete() if e.keyCode isnt KEYCODE.ENTER @wrap.toggleClass 'has-value', !!e.target.value @@ -221,10 +225,8 @@ class @SearchAutocomplete category = if item.category? then "#{item.category}: " else '' value = if item.value? then item.value else '' - html = "<span class='location-badge'> - <i class='location-text'>#{category}#{value}</i> - </span>" - @locationBadgeEl.html(html) + badgeText = "#{category}#{value}" + @locationBadgeEl.text(badgeText).show() @wrap.addClass('has-location-badge') restoreOriginalState: -> @@ -233,9 +235,8 @@ class @SearchAutocomplete for input in inputs @getElement("##{input}").val(@originalState[input]) - if @originalState._location is '' - @locationBadgeEl.empty() + @locationBadgeEl.hide() else @addLocationBadge( value: @originalState._location @@ -244,7 +245,7 @@ class @SearchAutocomplete @dropdown.removeClass 'open' badgePresent: -> - @locationBadgeEl.children().length + @locationBadgeEl.length resetSearchState: -> inputs = Object.keys @originalState @@ -257,7 +258,7 @@ class @SearchAutocomplete @getElement("##{input}").val('') removeLocationBadge: -> - @locationBadgeEl.empty() + @locationBadgeEl.hide() # Reset state @resetSearchState() diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee index 4a05bdccdb3..cca2b8a1fcc 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee @@ -3,10 +3,10 @@ class @ShortcutsDashboardNavigation extends Shortcuts constructor: -> super() - Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-activity')) - Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-issues')) - Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-merge_requests')) - Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-projects')) + Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-activity')) + Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-issues')) + Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-merge_requests')) + Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-projects')) @findAndFollowLink: (selector) -> link = $(selector).attr('href') diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee index ad9b3c1c6bf..c93bcf3ceec 100644 --- a/app/assets/javascripts/shortcuts_issuable.coffee +++ b/app/assets/javascripts/shortcuts_issuable.coffee @@ -6,12 +6,8 @@ class @ShortcutsIssuable extends ShortcutsNavigation super() Mousetrap.bind('a', @openSidebarDropdown.bind(@, 'assignee')) Mousetrap.bind('m', @openSidebarDropdown.bind(@, 'milestone')) - Mousetrap.bind('j', => - @prevIssue() - return false - ) - Mousetrap.bind('k', => - @nextIssue() + Mousetrap.bind('r', => + @replyWithSelectedText() return false ) Mousetrap.bind('e', => @@ -25,16 +21,6 @@ class @ShortcutsIssuable extends ShortcutsNavigation else @enabledHelp.push('.hidden-shortcut.issues') - prevIssue: -> - $prevBtn = $('.prev-btn') - if not $prevBtn.hasClass('disabled') - Turbolinks.visit($prevBtn.attr('href')) - - nextIssue: -> - $nextBtn = $('.next-btn') - if not $nextBtn.hasClass('disabled') - Turbolinks.visit($nextBtn.attr('href')) - replyWithSelectedText: -> if window.getSelection selected = window.getSelection().toString() diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index 860d4f438d0..2ce63c16428 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -4,23 +4,14 @@ expanded = 'page-sidebar-expanded' toggleSidebar = -> $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('header').toggleClass("header-collapsed header-expanded") - $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left") - $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) setTimeout ( -> niceScrollBars = $('.nicescroll').niceScroll(); niceScrollBars.updateScrollBar(); ), 300 -$(document).on("click", '.toggle-nav-collapse', (e) -> +$(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (e) -> e.preventDefault() toggleSidebar() ) - -$ -> - size = bp.getBreakpointSize() - - if size is "xs" or size is "sm" - if $('.page-with-sidebar').hasClass(expanded) - toggleSidebar() diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee index 1a430f3aa47..08d494aba9f 100644 --- a/app/assets/javascripts/subscription.js.coffee +++ b/app/assets/javascripts/subscription.js.coffee @@ -19,3 +19,8 @@ class @Subscription action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe' btn.find('span').text(action) @subscription_status.find('>div').toggleClass('hidden') + + if btn.attr('data-original-title') + btn.tooltip('hide') + .attr('data-original-title', action) + .tooltip('fixTitle') diff --git a/app/assets/javascripts/u2f/authenticate.js.coffee b/app/assets/javascripts/u2f/authenticate.js.coffee new file mode 100644 index 00000000000..6deb902c8de --- /dev/null +++ b/app/assets/javascripts/u2f/authenticate.js.coffee @@ -0,0 +1,63 @@ +# Authenticate U2F (universal 2nd factor) devices for users to authenticate with. +# +# State Flow #1: setup -> in_progress -> authenticated -> POST to server +# State Flow #2: setup -> in_progress -> error -> setup + +class @U2FAuthenticate + constructor: (@container, u2fParams) -> + @appId = u2fParams.app_id + @challenges = u2fParams.challenges + @signRequests = u2fParams.sign_requests + + start: () => + if U2FUtil.isU2FSupported() + @renderSetup() + else + @renderNotSupported() + + authenticate: () => + u2f.sign(@appId, @challenges, @signRequests, (response) => + if response.errorCode + error = new U2FError(response.errorCode) + @renderError(error); + else + @renderAuthenticated(JSON.stringify(response)) + , 10) + + ############# + # Rendering # + ############# + + templates: { + "notSupported": "#js-authenticate-u2f-not-supported", + "setup": '#js-authenticate-u2f-setup', + "inProgress": '#js-authenticate-u2f-in-progress', + "error": '#js-authenticate-u2f-error', + "authenticated": '#js-authenticate-u2f-authenticated' + } + + renderTemplate: (name, params) => + templateString = $(@templates[name]).html() + template = _.template(templateString) + @container.html(template(params)) + + renderSetup: () => + @renderTemplate('setup') + @container.find('#js-login-u2f-device').on('click', @renderInProgress) + + renderInProgress: () => + @renderTemplate('inProgress') + @authenticate() + + renderError: (error) => + @renderTemplate('error', {error_message: error.message()}) + @container.find('#js-u2f-try-again').on('click', @renderSetup) + + renderAuthenticated: (deviceResponse) => + @renderTemplate('authenticated') + # Prefer to do this instead of interpolating using Underscore templates + # because of JSON escaping issues. + @container.find("#js-device-response").val(deviceResponse) + + renderNotSupported: () => + @renderTemplate('notSupported') diff --git a/app/assets/javascripts/u2f/error.js.coffee b/app/assets/javascripts/u2f/error.js.coffee new file mode 100644 index 00000000000..1a2fc3e757f --- /dev/null +++ b/app/assets/javascripts/u2f/error.js.coffee @@ -0,0 +1,13 @@ +class @U2FError + constructor: (@errorCode) -> + @httpsDisabled = (window.location.protocol isnt 'https:') + console.error("U2F Error Code: #{@errorCode}") + + message: () => + switch + when (@errorCode is u2f.ErrorCodes.BAD_REQUEST and @httpsDisabled) + "U2F only works with HTTPS-enabled websites. Contact your administrator for more details." + when @errorCode is u2f.ErrorCodes.DEVICE_INELIGIBLE + "This device has already been registered with us." + else + "There was a problem communicating with your device." diff --git a/app/assets/javascripts/u2f/register.js.coffee b/app/assets/javascripts/u2f/register.js.coffee new file mode 100644 index 00000000000..74472cfa120 --- /dev/null +++ b/app/assets/javascripts/u2f/register.js.coffee @@ -0,0 +1,63 @@ +# Register U2F (universal 2nd factor) devices for users to authenticate with. +# +# State Flow #1: setup -> in_progress -> registered -> POST to server +# State Flow #2: setup -> in_progress -> error -> setup + +class @U2FRegister + constructor: (@container, u2fParams) -> + @appId = u2fParams.app_id + @registerRequests = u2fParams.register_requests + @signRequests = u2fParams.sign_requests + + start: () => + if U2FUtil.isU2FSupported() + @renderSetup() + else + @renderNotSupported() + + register: () => + u2f.register(@appId, @registerRequests, @signRequests, (response) => + if response.errorCode + error = new U2FError(response.errorCode) + @renderError(error); + else + @renderRegistered(JSON.stringify(response)) + , 10) + + ############# + # Rendering # + ############# + + templates: { + "notSupported": "#js-register-u2f-not-supported", + "setup": '#js-register-u2f-setup', + "inProgress": '#js-register-u2f-in-progress', + "error": '#js-register-u2f-error', + "registered": '#js-register-u2f-registered' + } + + renderTemplate: (name, params) => + templateString = $(@templates[name]).html() + template = _.template(templateString) + @container.html(template(params)) + + renderSetup: () => + @renderTemplate('setup') + @container.find('#js-setup-u2f-device').on('click', @renderInProgress) + + renderInProgress: () => + @renderTemplate('inProgress') + @register() + + renderError: (error) => + @renderTemplate('error', {error_message: error.message()}) + @container.find('#js-u2f-try-again').on('click', @renderSetup) + + renderRegistered: (deviceResponse) => + @renderTemplate('registered') + # Prefer to do this instead of interpolating using Underscore templates + # because of JSON escaping issues. + @container.find("#js-device-response").val(deviceResponse) + + renderNotSupported: () => + @renderTemplate('notSupported') diff --git a/app/assets/javascripts/u2f/util.js.coffee.erb b/app/assets/javascripts/u2f/util.js.coffee.erb new file mode 100644 index 00000000000..d59341c38b9 --- /dev/null +++ b/app/assets/javascripts/u2f/util.js.coffee.erb @@ -0,0 +1,15 @@ +# Helper class for U2F (universal 2nd factor) device registration and authentication. + +class @U2FUtil + @isU2FSupported: -> + if @testMode + true + else + gon.u2f.browser_supports_u2f + + @enableTestMode: -> + @testMode = true + +<% if Rails.env.test? %> +U2FUtil.enableTestMode(); +<% end %> diff --git a/app/assets/javascripts/user_tabs.js.coffee b/app/assets/javascripts/user_tabs.js.coffee index c2aeffe2381..29dad21faed 100644 --- a/app/assets/javascripts/user_tabs.js.coffee +++ b/app/assets/javascripts/user_tabs.js.coffee @@ -26,6 +26,10 @@ # Personal projects # </a> # </li> +# <li class="snippets-tab"> +# <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets"> +# </a> +# </li> # </ul> # # <div class="tab-content"> @@ -41,6 +45,9 @@ # <div class="tab-pane" id="projects"> # Projects content # </div> +# <div class="tab-pane" id="snippets"> +# Snippets content +# </div> # </div> # # <div class="loading-status"> @@ -100,7 +107,7 @@ class @UserTabs if action is 'activity' @loadActivities(source) - if action in ['groups', 'contributed', 'projects'] + if action in ['groups', 'contributed', 'projects', 'snippets'] @loadTab(source, action) loadTab: (source, action) -> @@ -115,6 +122,9 @@ class @UserTabs @parentEl.find(tabSelector).html(data.html) @loaded[action] = true + # Fix tooltips + gl.utils.localTimeAgo($('.js-timeago', tabSelector)) + loadActivities: (source) -> return if @loaded['activity'] is true diff --git a/app/assets/javascripts/users/application.js.coffee b/app/assets/javascripts/users/application.js.coffee new file mode 100644 index 00000000000..647ffbf5f45 --- /dev/null +++ b/app/assets/javascripts/users/application.js.coffee @@ -0,0 +1,8 @@ +# This is a manifest file that'll be compiled into including all the files listed below. +# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +# be included in the compiled file accessible from http://example.com/assets/application.js +# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +# the compiled file. +# +#= require d3 +#= require_tree . diff --git a/app/assets/javascripts/users/calendar.js.coffee b/app/assets/javascripts/users/calendar.js.coffee new file mode 100644 index 00000000000..26a26061539 --- /dev/null +++ b/app/assets/javascripts/users/calendar.js.coffee @@ -0,0 +1,198 @@ +class @Calendar + constructor: (timestamps, @calendar_activities_path) -> + @currentSelectedDate = '' + @daySpace = 1 + @daySize = 15 + @daySizeWithSpace = @daySize + (@daySpace * 2) + @monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + @months = [] + @highestValue = 0 + + # Get the highest value from the timestampes + _.each timestamps, (count) => + if count > @highestValue + @highestValue = count + + # Loop through the timestamps to create a group of objects + # The group of objects will be grouped based on the day of the week they are + @timestampsTmp = [] + i = 0 + group = 0 + _.each timestamps, (count, date) => + newDate = new Date parseInt(date) * 1000 + day = newDate.getDay() + + # Create a new group array if this is the first day of the week + # or if is first object + if (day is 0 and i isnt 0) or i is 0 + @timestampsTmp.push [] + group++ + + innerArray = @timestampsTmp[group-1] + + # Push to the inner array the values that will be used to render map + innerArray.push + count: count + date: newDate + day: day + + i++ + + # Init color functions + @color = @initColor() + @colorKey = @initColorKey() + + # Init the svg element + @renderSvg(group) + @renderDays() + @renderMonths() + @renderDayTitles() + @renderKey() + + @initTooltips() + + renderSvg: (group) -> + @svg = d3.select '.js-contrib-calendar' + .append 'svg' + .attr 'width', (group + 1) * @daySizeWithSpace + .attr 'height', 167 + .attr 'class', 'contrib-calendar' + + renderDays: -> + @svg.selectAll 'g' + .data @timestampsTmp + .enter() + .append 'g' + .attr 'transform', (group, i) => + _.each group, (stamp, a) => + if a is 0 and stamp.day is 0 + month = stamp.date.getMonth() + x = (@daySizeWithSpace * i + 1) + @daySizeWithSpace + lastMonth = _.last(@months) + if lastMonth? + lastMonthX = lastMonth.x + + if !lastMonth? + @months.push + month: month + x: x + else if month isnt lastMonth.month and x - @daySizeWithSpace isnt lastMonthX + @months.push + month: month + x: x + + "translate(#{(@daySizeWithSpace * i + 1) + @daySizeWithSpace}, 18)" + .selectAll 'rect' + .data (stamp) -> + stamp + .enter() + .append 'rect' + .attr 'x', '0' + .attr 'y', (stamp, i) => + (@daySizeWithSpace * stamp.day) + .attr 'width', @daySize + .attr 'height', @daySize + .attr 'title', (stamp) => + contribText = 'No contributions' + + if stamp.count > 0 + contribText = "#{stamp.count} contribution#{if stamp.count > 1 then 's' else ''}" + + date = dateFormat(stamp.date, 'mmm d, yyyy') + + "#{contribText}<br />#{date}" + .attr 'class', 'user-contrib-cell js-tooltip' + .attr 'fill', (stamp) => + if stamp.count isnt 0 + @color(stamp.count) + else + '#ededed' + .attr 'data-container', 'body' + .on 'click', @clickDay + + renderDayTitles: -> + days = [{ + text: 'M' + y: 29 + (@daySizeWithSpace * 1) + }, { + text: 'W' + y: 29 + (@daySizeWithSpace * 3) + }, { + text: 'F' + y: 29 + (@daySizeWithSpace * 5) + }] + @svg.append 'g' + .selectAll 'text' + .data days + .enter() + .append 'text' + .attr 'text-anchor', 'middle' + .attr 'x', 8 + .attr 'y', (day) -> + day.y + .text (day) -> + day.text + .attr 'class', 'user-contrib-text' + + renderMonths: -> + @svg.append 'g' + .selectAll 'text' + .data @months + .enter() + .append 'text' + .attr 'x', (date) -> + date.x + .attr 'y', 10 + .attr 'class', 'user-contrib-text' + .text (date) => + @monthNames[date.month] + + renderKey: -> + keyColors = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)] + @svg.append 'g' + .attr 'transform', "translate(18, #{@daySizeWithSpace * 8 + 16})" + .selectAll 'rect' + .data keyColors + .enter() + .append 'rect' + .attr 'width', @daySize + .attr 'height', @daySize + .attr 'x', (color, i) => + @daySizeWithSpace * i + .attr 'y', 0 + .attr 'fill', (color) -> + color + + initColor: -> + d3.scale + .linear() + .range(['#acd5f2', '#254e77']) + .domain([0, @highestValue]) + + initColorKey: -> + d3.scale + .linear() + .range(['#acd5f2', '#254e77']) + .domain([0, 3]) + + clickDay: (stamp) => + if @currentSelectedDate isnt stamp.date + @currentSelectedDate = stamp.date + formatted_date = @currentSelectedDate.getFullYear() + "-" + (@currentSelectedDate.getMonth()+1) + "-" + @currentSelectedDate.getDate() + + $.ajax + url: @calendar_activities_path + data: + date: formatted_date + cache: false + dataType: 'html' + beforeSend: -> + $('.user-calendar-activities').html '<div class="text-center"><i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i></div>' + success: (data) -> + $('.user-calendar-activities').html data + else + $('.user-calendar-activities').html '' + + initTooltips: -> + $('.js-contrib-calendar .js-tooltip').tooltip + html: true diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index b80b1b861cc..88246b0feb8 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -93,6 +93,8 @@ class @UsersSelect $dropdown.glDropdown( data: (term, callback) => + isAuthorFilter = $('.js-author-search') + @users term, (users) => if term.length is 0 showDivider = 0 @@ -138,7 +140,7 @@ class @UsersSelect toggleLabel: (selected) -> if selected && 'id' of selected - selected.name + if selected.text then selected.text else selected.name else defaultLabel @@ -147,7 +149,7 @@ class @UsersSelect hidden: (e) -> $selectbox.hide() # display:block overrides the hide-collapse rule - $value.removeAttr('style') + $value.css('display', '') clicked: (user) -> page = $('body').data 'page' diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 69b3b6586de..8b93665d085 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -8,9 +8,7 @@ *= require select2 *= require_self *= require dropzone/basic - *= require cal-heatmap *= require cropper.css - *= require animate */ /* diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 560de9fc0bd..3cbddc59f11 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -5,6 +5,7 @@ @import 'framework/tw_bootstrap'; @import "framework/layout"; +@import "framework/animations.scss"; @import "framework/avatar.scss"; @import "framework/blocks.scss"; @import "framework/buttons.scss"; diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss new file mode 100644 index 00000000000..1fec61bdba1 --- /dev/null +++ b/app/assets/stylesheets/framework/animations.scss @@ -0,0 +1,72 @@ +// This file is based off animate.css 3.5.1, available here: +// https://github.com/daneden/animate.css/blob/3.5.1/animate.css +// +// animate.css - http://daneden.me/animate +// Version - 3.5.1 +// Licensed under the MIT license - http://opensource.org/licenses/MIT +// +// Copyright (c) 2016 Daniel Eden + +.animated { + -webkit-animation-duration: 1s; + animation-duration: 1s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} + +.animated.infinite { + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; +} + +.animated.hinge { + -webkit-animation-duration: 2s; + animation-duration: 2s; +} + +.animated.flipOutX, +.animated.flipOutY, +.animated.bounceIn, +.animated.bounceOut { + -webkit-animation-duration: .75s; + animation-duration: .75s; +} + +@-webkit-keyframes pulse { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +@keyframes pulse { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +.pulse { + -webkit-animation-name: pulse; + animation-name: pulse; +} diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index f5ce70b606b..bb8d71fbae8 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -45,6 +45,7 @@ &.s32 { font-size: 20px; line-height: 32px; } &.s40 { font-size: 16px; line-height: 40px; } &.s60 { font-size: 32px; line-height: 60px; } + &.s70 { font-size: 34px; line-height: 70px; } &.s90 { font-size: 36px; line-height: 90px; } &.s110 { font-size: 40px; line-height: 112px; font-weight: 300; } &.s140 { font-size: 72px; line-height: 140px; } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 434a26d57c6..fab96404a6c 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -24,8 +24,8 @@ background-color: $background-color; padding: $gl-padding; margin-bottom: 0; - border-top: 1px solid $border-color; - border-bottom: 1px solid $border-color; + border-top: 1px solid $white-dark; + border-bottom: 1px solid $white-dark; color: $gl-gray; &.oneline-block { @@ -61,6 +61,11 @@ margin-bottom: -$gl-padding; } + &.content-component-block { + padding: 11px 0; + background-color: $white-light; + } + .title { color: $gl-text-color; } @@ -110,9 +115,9 @@ .cover-title { color: $gl-header-color; margin: 0; - font-size: 23px; + font-size: 24px; font-weight: normal; - margin: 16px 0 5px; + margin-bottom: 5px; color: #4c4e54; font-size: 23px; line-height: 1.1; @@ -137,7 +142,6 @@ } .cover-desc { - padding: 0 $gl-padding 3px; color: $gl-text-color; &.username:last-child { @@ -205,7 +209,7 @@ .content-block { padding: $gl-padding 0; - border-bottom: 1px solid $border-color; + border-bottom: 1px solid $white-dark; &.oneline-block { line-height: 36px; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index eaf85bb17ca..1e3083cce55 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -16,6 +16,19 @@ @include btn-default; } +@mixin btn-outline($background, $text, $border, $hover-background, $hover-text, $hover-border) { + background-color: $background; + color: $text; + border-color: $border; + + &:hover, + &:focus { + background-color: $hover-background; + color: $hover-text; + border-color: $hover-border;; + } +} + @mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) { background-color: $light; border-color: $border-light; @@ -66,6 +79,23 @@ @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $btn-white-active); } +@mixin btn-with-margin { + margin-left: $btn-side-margin; + float: left; + + &.inline { + float: none; + } + + &.btn-sm { + margin-left: $btn-sm-side-margin; + } + + &.btn-xs { + margin-left: $btn-xs-side-margin; + } +} + .btn { @include btn-default; @include btn-white; @@ -106,11 +136,14 @@ @include btn-blue; } - &.btn-close, &.btn-warning { @include btn-orange; } + &.btn-close { + @include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light); + } + &.btn-danger, &.btn-remove, &.btn-red { @@ -126,15 +159,9 @@ } &.btn-grouped { - margin-right: 7px; - float: left; - &:last-child { - margin-right: 0; - } - &.btn-xs { - margin-right: 3px; - } + @include btn-with-margin; } + &.disabled { pointer-events: auto !important; } @@ -176,11 +203,7 @@ .btn-group { &.btn-grouped { - margin-right: 7px; - float: left; - &:last-child { - margin-right: 0; - } + @include btn-with-margin; } } diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 11f39d583bd..8642b7530e2 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -1,70 +1,44 @@ .calender-block { + padding-left: 0; + padding-right: 0; + @media (min-width: $screen-sm-min) and (max-width: $screen-lg-min) { overflow-x: scroll; } } .user-calendar-activities { - .calendar_onclick_hr { - padding: 0; - margin: 10px 0; - } - .str-truncated { max-width: 70%; } - .text-expander { - background: #eee; - color: #555; - padding: 0 5px; - cursor: pointer; - margin-left: 4px; - &:hover { - background-color: #ddd; - } + .user-calendar-activities-loading { + font-size: 24px; } } -/** -* This overwrites the default values of the cal-heatmap gem -*/ -.calendar { - .qi { - fill: #fff; - } - - .q1 { - fill: #ededed !important; - } +.user-calendar { + text-align: center; - .q2 { - fill: #acd5f2 !important; - } - - .q3 { - fill: #7fa8d1 !important; - } - - .q4 { - fill: #49729b !important; - } - - .q5 { - fill: #254e77 !important; + .calendar { + display: inline-block; } +} - .future { - visibility: hidden; +.user-contrib-cell { + &:hover { + cursor: pointer; + stroke: #000; } +} - .domain-background { - fill: none; - shape-rendering: crispedges; - } +.user-contrib-text { + font-size: 12px; + fill: #959494; +} - .ch-tooltip { - padding: 3px; - font-weight: 550; - } +.calendar-hint { + margin-top: -23px; + float: right; + font-size: 12px; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 3386523dbf7..f8aecd0558d 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -289,7 +289,7 @@ table { text-shadow: none; @media (min-width: $screen-sm-min) { - margin-top: 11px; + margin-top: 8px; } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 4bf3a050403..d4d579a083d 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -122,10 +122,9 @@ a { display: block; position: relative; - padding-left: 10px; - padding-right: 10px; + padding: 5px 10px; color: $dropdown-link-color; - line-height: 34px; + line-height: initial; text-overflow: ellipsis; border-radius: 2px; white-space: nowrap; @@ -154,7 +153,7 @@ color: $dropdown-header-color; font-size: 13px; line-height: 22px; - padding: 0 10px 10px; + padding: 0 10px; } .separator + .dropdown-header { @@ -162,6 +161,20 @@ } } +.dropdown-menu-large { + width: 340px; +} + +.dropdown-menu-no-wrap { + a { + white-space: normal; + } +} + +.dropdown-menu-full-width { + width: 100%; +} + .dropdown-menu-paging { .dropdown-page-two, .dropdown-menu-back { @@ -228,13 +241,11 @@ a { padding-left: 25px; - &.is-active { + &.is-indeterminate, &.is-active { &::before { - content: "\f00c"; position: absolute; left: 5px; - top: 50%; - margin-top: -7px; + top: 8px; font: normal normal normal 14px/1 FontAwesome; font-size: inherit; text-rendering: auto; @@ -242,6 +253,14 @@ -moz-osx-font-smoothing: grayscale; } } + + &.is-indeterminate::before { + content: "\f068"; + } + + &.is-active::before { + content: "\f00c"; + } } } @@ -521,3 +540,14 @@ background-color: $calendar-unselectable-bg; } } + +.dropdown-menu-inner-title { + display: block; + color: $gl-title-color; + font-weight: 600; +} + +.dropdown-menu-inner-content { + display: block; + color: $gl-placeholder-color; +} diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 61d9954c6c8..71a9f79be3e 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -5,6 +5,10 @@ .file-holder { border: 1px solid $border-color; + &.file-holder-no-border { + border: 0; + } + &.readme-holder { margin: $gl-padding-top 0; } @@ -23,8 +27,17 @@ word-wrap: break-word; border-radius: 3px 3px 0 0; + &.file-title-clear { + padding-left: 0; + padding-right: 0; + background-color: transparent; + + .file-actions { + right: 0; + } + } + .file-actions { - float: right; position: absolute; top: 5px; right: 15px; @@ -36,22 +49,6 @@ } } - .filename { - &.old { - display: inline-block; - span.idiff { - background-color: #f8cbcb; - } - } - - &.new { - display: inline-block; - span.idiff { - background-color: #a6f3a6; - } - } - } - a:not(.btn) { color: $gl-dark-link-color; } diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 558b133f593..43d55661541 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -28,10 +28,6 @@ input[type='text'].danger { } label { - &.control-label { - @extend .col-sm-2; - } - &.inline-label { margin: 0; } @@ -41,6 +37,10 @@ label { } } +.control-label { + @extend .col-sm-2; +} + .inline-input-group { width: 250px; } @@ -76,6 +76,7 @@ label { .form-control { @include box-shadow(none); border-radius: 3px; + padding: $gl-vert-padding $gl-input-padding; } .select-wrapper { diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index c83cf881596..408d4a68e1e 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -8,31 +8,16 @@ */ @mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { .page-with-sidebar { - .header-logo { - background-color: $color; - border-color: $color; - a { - color: $color-light; - - h3 { - color: $color-light; - } - } + .collapse-nav a { + color: $color-light; + background: $color; &:hover { - background-color: $color-darker; - a { - color: #fff; - } + color: $white-light; } } - .collapse-nav a { - color: #fff; - background: $color; - } - .sidebar-wrapper { background: $color-darker; @@ -42,7 +27,7 @@ &:hover { background-color: $color-dark; - color: #fff; + color: $white-light; text-decoration: none; } } @@ -60,10 +45,20 @@ color: $color-light; } + path, + polygon { + fill: $color-light; + } + .count { color: $color-light; background: $color-dark; } + + svg { + position: relative; + top: 3px; + } } &.separate-item { @@ -71,7 +66,7 @@ } &.active a { - color: #fff; + color: $white-light; background: $color-dark; &.no-highlight { @@ -79,16 +74,24 @@ } i { - color: #fff + color: $white-light + } + + path, + polygon { + fill: $white-light; } } } } } +$theme-charcoal: #3d454d; +$theme-charcoal-dark: #383f45; +$theme-charcoal-text: #b9bbbe; + $theme-blue: #2980b9; -$theme-charcoal: #333c47; -$theme-graphite: #888; +$theme-graphite: #666; $theme-gray: #373737; $theme-green: #019875; $theme-violet: #548; @@ -99,11 +102,11 @@ body { } &.ui_charcoal { - @include gitlab-theme(#c5d0de, $theme-charcoal, #2b333d, #24272d); + @include gitlab-theme($theme-charcoal-text, #485157, $theme-charcoal, $theme-charcoal-dark); } &.ui_graphite { - @include gitlab-theme(#ccc, $theme-graphite, #777, #666); + @include gitlab-theme(#ccc, #777, $theme-graphite, #555); } &.ui_gray { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 97f9d582007..63996ea44f6 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -6,12 +6,12 @@ header { transition-duration: .3s; &.navbar-empty { - height: 58px; + height: $header-height; background: #fff; - border-bottom: 1px solid #eee; + border-bottom: 1px solid $btn-gray-hover; .center-logo { - margin: 11px 0; + margin: 8px 0; text-align: center; #tanuki-logo, img { @@ -22,14 +22,18 @@ header { } &.navbar-gitlab { - padding: 0 20px; + padding: 0 16px; z-index: 100; margin-bottom: 0; - min-height: $header-height; + height: $header-height; background-color: $background-color; border: none; border-bottom: 1px solid $border-color; + @media (max-width: $screen-xs-min) { + padding: 0 16px; + } + &.with-horizontal-nav { border-bottom: none; } @@ -60,22 +64,53 @@ header { margin: 6px 0; border-radius: 0; position: absolute; - right: 2px; + right: -10px; + padding: 6px 10px; &:hover { - background-color: #eee; + background-color: $btn-gray-hover; } + &.active { color: $gl-icon-color; } } } + + &.header-collapsed { + padding: 0 16px; + + .side-nav-toggle { + display: block; + } + } + + .side-nav-toggle { + display: none; + position: absolute; + left: -10px; + margin: 6px 0; + font-size: 18px; + padding: 6px 10px; + border: none; + background-color: $background-color; + + &:hover { + background-color: $btn-gray-hover; + } + + &:focus { + outline: none; + } + } } .header-content { position: relative; height: $header-height; padding-right: 40px; + padding-left: 30px; + transition-duration: .3s; @media (min-width: $screen-sm-min) { padding-right: 0; @@ -85,9 +120,29 @@ header { margin-top: -5px; } + .header-logo { + position: absolute; + left: 50%; + margin-left: -18px; + top: 7px; + transition-duration: .3s; + z-index: 999; + + &:hover { + cursor: pointer; + } + + @media (max-width: $screen-xs-max) { + right: 25px; + left: auto; + } + } + .title { margin: 0; font-size: 19px; + max-width: 400px; + display: inline-block; line-height: $header-height; font-weight: normal; color: $gl-text-color; @@ -96,6 +151,10 @@ header { vertical-align: top; white-space: nowrap; + @media (max-width: $screen-sm-max) { + max-width: 190px; + } + a { color: $gl-text-color; &:hover { @@ -123,6 +182,10 @@ header { .navbar-collapse { float: right; border-top: none; + + @media (max-width: $screen-xs-max) { + float: none; + } } } @@ -135,23 +198,24 @@ header { } } -@mixin collapsed-header { - margin-left: $sidebar_collapsed_width; -} - .header-collapsed { - margin-left: $sidebar_collapsed_width; + margin-left: 0; - @media (min-width: $screen-md-min) { - @include collapsed-header; + .header-content { + + @media (min-width: $screen-sm-max) { + padding-left: 30px; + transition-duration: .3s; + } } } -.header-expanded { - margin-left: $sidebar_collapsed_width; +.tanuki-shape { + transition: all 0.8s; - @media (min-width: $screen-md-min) { - margin-left: $sidebar_width; + &:hover, &.highlight { + fill: rgb(255, 255, 255); + transition: all 0.1s; } } diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index 525ed81b059..30a5b837d69 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -2,6 +2,7 @@ font-family: $regular_font; font-size: $font-size-base; + &.ui-datepicker, &.ui-datepicker-inline { border: 1px solid #ddd; padding: 10px; @@ -10,6 +11,25 @@ .ui-datepicker-header { background: #fff; border-color: #ddd; + + .ui-datepicker-prev, + .ui-datepicker-next { + top: 4px; + } + + .ui-datepicker-prev { + left: 2px; + } + + .ui-datepicker-next { + right: 2px; + } + + .ui-state-hover { + background: transparent; + border: 0; + cursor: pointer; + } } .ui-datepicker-calendar td a { @@ -36,21 +56,18 @@ } .ui-state-highlight { - border: 1px solid #eee; - background: #eee; + border: 0; + background: transparent; } - .ui-state-active { - border: 1px solid $gl-primary; - background: $gl-primary; - color: #fff; - } - - .ui-state-hover, - .ui-state-focus { - border: 1px solid $row-hover; - background: $row-hover; - color: #333; + .ui-datepicker-calendar { + .ui-state-active, + .ui-state-hover, + .ui-state-focus { + border: 1px solid $gl-primary; + background: $gl-primary; + color: #fff; + } } } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index b17c8bcbb1e..b34ec16cdba 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -137,10 +137,30 @@ ul.content-list { padding-top: 1px; float: right; - .btn { - padding: 10px 14px; + > .btn, + > .btn-group { + margin-right: $gl-padding-top; + display: inline-block; + margin-top: 4px; + margin-bottom: 4px; + + &:last-child { + margin-right: 0; + } } } + + // When dragging a list item + &.ui-sortable-helper { + border-bottom: none; + } + + &.list-placeholder { + background-color: $gray-light; + border: dotted 1px $gray-dark; + margin: 1px 0; + min-height: 30px; + } } } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 250d6309291..828e7224231 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -2,18 +2,10 @@ * Generic mixins */ @mixin box-shadow($shadow) { - -webkit-box-shadow: $shadow; - -moz-box-shadow: $shadow; - -ms-box-shadow: $shadow; - -o-box-shadow: $shadow; box-shadow: $shadow; } @mixin border-radius($radius) { - -webkit-border-radius: $radius; - -moz-border-radius: $radius; - -ms-border-radius: $radius; - -o-border-radius: $radius; border-radius: $radius; } diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 33cbee85987..d4e5cc819a4 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -48,10 +48,6 @@ display: block; } - .project-home-desc { - font-size: 21px; - } - .project-repo-buttons, .git-clone-holder { display: none; @@ -70,10 +66,6 @@ display: none; } - %ul.notes .note-role, .note-actions { - display: none; - } - .nav-links, .nav-links { li a { font-size: 14px; diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 7c18e93a261..4de89daeb36 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -1,3 +1,35 @@ +@mixin fade($gradient-direction, $rgba, $gradient-color) { + visibility: visible; + opacity: 1; + z-index: 2; + position: absolute; + bottom: 12px; + width: 43px; + height: 30px; + transition-duration: .3s; + -webkit-transform: translateZ(0); + background: -webkit-linear-gradient($gradient-direction, $rgba, $gradient-color 45%); + background: -o-linear-gradient($gradient-direction, $rgba, $gradient-color 45%); + background: -moz-linear-gradient($gradient-direction, $rgba, $gradient-color 45%); + background: linear-gradient($gradient-direction, $rgba, $gradient-color 45%); + + &.end-scroll { + visibility: hidden; + opacity: 0; + transition-duration: .3s; + } +} + +@mixin scrolling-links() { + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + &::-webkit-scrollbar { + display: none; + } +} + .nav-links { padding: 0; margin: 0; @@ -10,8 +42,7 @@ a { display: inline-block; - padding: 14px; - padding-top: $gl-padding; + padding: $gl-btn-padding; padding-bottom: 11px; margin-bottom: -1px; font-size: 15px; @@ -36,6 +67,28 @@ color: #78a; } } + + &.sub-nav { + text-align: center; + background-color: $background-color; + + .container-fluid { + background-color: $background-color; + } + + li { + + a { + margin: 0; + padding: 11px 10px 9px; + } + + &.active a { + border-bottom: none; + color: $link-underline-blue; + } + } + } } .top-area { @@ -50,6 +103,10 @@ width: 50%; line-height: 28px; + &.wiki-page { + padding: 16px 10px 11px; + } + /* Small devices (phones, tablets, 768px and lower) */ @media (max-width: $screen-sm-min) { width: 100%; @@ -73,6 +130,10 @@ margin-bottom: 0; border-bottom: none; + li a { + padding: 16px 10px 11px; + } + /* Small devices (phones, tablets, 768px and lower) */ @media (max-width: $screen-sm-max) { width: 100%; @@ -119,7 +180,7 @@ } input { - height: 34px; + height: 35px; display: inline-block; position: relative; top: 2px; @@ -148,7 +209,7 @@ @media (max-width: $screen-xs-max) { padding-bottom: 0; - + width: 100%; .btn, form, .dropdown, .dropdown-menu-toggle, .form-control { margin: 0 0 10px; display: block; @@ -179,16 +240,6 @@ margin: 0; } } - - /* Small devices (tablets, 768px and lower) */ - @media (max-width: $screen-sm-max) { - width: 100%; - text-align: left; - - input { - width: 300px; - } - } } } @@ -196,14 +247,23 @@ position: fixed; top: $header-height; width: 100%; - z-index: 1; + z-index: 11; background: $background-color; border-bottom: 1px solid $border-color; transition-duration: .3s; + text-align: center; + + .container-fluid { + position: relative; + } .controls { float: right; - padding: 7px 5px 0 0; + padding: 7px 0 0; + + @media (max-width: $screen-xs-max) { + display: none; + } i { color: $layout-link-gray; @@ -221,14 +281,44 @@ .dropdown { margin-left: 7px; + + @media (max-width: $screen-xs-min) { + margin-left: 0; + } + + li.active { + font-weight: bold; + } } } .nav-links { + @include scrolling-links(); border-bottom: none; height: 51px; - white-space: nowrap; - overflow-x: auto; + + svg { + position: relative; + top: 2px; + margin-right: 2px; + height: 15px; + width: auto; + + path, + polygon { + fill: $layout-link-gray; + } + } + + .fade-right { + @include fade(left, rgba(250, 250, 250, 0.4), $background-color); + right: 0; + } + + .fade-left { + @include fade(right, rgba(250, 250, 250, 0.4), $background-color); + left: 0; + } li { @@ -241,9 +331,17 @@ } &.active { + a, i { color: $black; } + + svg { + path, + polygon { + fill: $black; + } + } } .badge { @@ -252,8 +350,80 @@ } } + .nav-control { + + .fade-right { + @media (min-width: $screen-xs-max) { + right: 68px; + } + @media (max-width: $screen-xs-min) { + right: 0; + } + } + } +} + +.scrolling-tabs-container { + position: relative; + + .nav-links { + @include scrolling-links(); + + .fade-right { + @include fade(left, rgba(255, 255, 255, 0.4), $background-color); + right: 0; + } + + .fade-left { + @include fade(right, rgba(255, 255, 255, 0.4), $background-color); + left: 0; + } + } +} + +.nav-block { + position: relative; + + .nav-links { + @include scrolling-links(); + + .fade-right { + @include fade(left, rgba(255, 255, 255, 0.4), $white-light); + right: 0; + } + + .fade-left { + @include fade(right, rgba(255, 255, 255, 0.4), $white-light); + left: 0; + } + + &.event-filter { + .fade-right { + visibility: hidden; + + @media (max-width: $screen-xs-max) { + visibility: visible; + } + } + } + } } .page-with-layout-nav { - margin-top: 50px; + margin-top: $header-height + 2; + + .right-sidebar { + top: ($header-height * 2) + 2; + } +} + +.activities { + + .nav-block { + border-bottom: 1px solid $border-color; + + .nav-links { + border-bottom: none; + } + } } diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 6efc6ec1e4b..f242706ebe4 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -8,7 +8,7 @@ background: #fff; border-color: $input-border; height: 35px; - padding: $gl-vert-padding $gl-btn-padding; + padding: $gl-vert-padding $gl-input-padding; font-size: $gl-font-size; line-height: 1.42857143; border-radius: $border-radius-base; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index e940fd7286e..b7ec3f70bfb 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -1,11 +1,3 @@ -#logo { - z-index: 2; - position: absolute; - width: 58px; - cursor: pointer; - margin-top: 8px; -} - .page-with-sidebar { padding-top: $header-height; transition-duration: .3s; @@ -20,12 +12,6 @@ height: 100%; transition-duration: .3s; } - - .gitlab-text-container-link { - z-index: 1; - position: absolute; - left: 0; - } } .sidebar-wrapper { @@ -49,58 +35,11 @@ } .sidebar-wrapper { - .header-logo { - border-bottom: 1px solid transparent; - float: left; - height: $header-height; - width: $sidebar_width; - position: fixed; - z-index: 999; - overflow: hidden; - transition-duration: .3s; - - a { - float: left; - height: $header-height; - width: 100%; - padding-left: 22px; - overflow: hidden; - outline: none; - transition-duration: .3s; - - img { - width: 36px; - height: 36px; - } - - #tanuki-logo, img { - float: left; - } - - .gitlab-text-container { - width: 230px; - - h3 { - width: 158px; - float: left; - margin: 0; - margin-left: 50px; - font-size: 19px; - line-height: 50px; - font-weight: normal; - } - } - } - - &:hover { - background-color: #eee; - } - } .sidebar-user { - padding: 9px 22px; + padding: 15px 22px; position: fixed; - bottom: 40px; + bottom: 0; width: $sidebar_width; overflow: hidden; transition-duration: .3s; @@ -126,8 +65,8 @@ .nav-sidebar { - margin-top: 14 + $header-height; - margin-bottom: 100px; + margin-top: 22 + $header-height; + margin-bottom: 116px; transition-duration: .3s; list-style: none; overflow: hidden; @@ -145,13 +84,12 @@ } a { - padding: 7px 15px; + width: $sidebar_width; + padding: 7px 15px 7px 23px; font-size: $gl-font-size; line-height: 24px; - color: $gray; display: block; text-decoration: none; - padding-left: 23px; font-weight: normal; outline: none; @@ -164,16 +102,12 @@ } i { - width: 16px; - color: $gray-light; - margin-right: 13px; + font-size: 16px; } - .count { - float: right; - background: #eee; - padding: 0 8px; - @include border-radius(6px); + i, + svg { + margin-right: 13px; } &.back-link i { @@ -181,6 +115,12 @@ } } } + + .count { + float: right; + padding: 0 8px; + @include border-radius(6px); + } } .sidebar-subnav { @@ -195,11 +135,12 @@ .collapse-nav a { width: $sidebar_width; position: fixed; - bottom: 0; + top: 0; left: 0; - font-size: 13px; + padding: 5px 0; + font-size: 18px; background: transparent; - height: 40px; + height: 50px; text-align: center; line-height: 40px; transition-duration: .3s; @@ -210,26 +151,20 @@ } } +.sidebar-wrapper { + &.hidden-nav { + width: 0; + } +} + .page-sidebar-collapsed { - padding-left: $sidebar_collapsed_width; + padding-left: 0; .sidebar-wrapper { - width: $sidebar_collapsed_width; - - .header-logo { - width: $sidebar_collapsed_width; - - a { - padding-left: ($sidebar_collapsed_width - 36) / 2; - - .gitlab-text-container { - display: none; - } - } - } + width: 0; .nav-sidebar { - width: $sidebar_collapsed_width; + width: 0; li { width: auto; @@ -243,29 +178,29 @@ } .collapse-nav a { - width: $sidebar_collapsed_width; + width: 0; + + i { + display: none; + } } .sidebar-user { - padding-left: ($sidebar_collapsed_width - 36) / 2; - width: $sidebar_collapsed_width; + width: 0; + padding-left: 0; + padding-right: 0; .username { display: none; } } } - - .layout-nav { - padding-right: $sidebar_collapsed_width; - } } .page-sidebar-expanded { - padding-left: $sidebar_collapsed_width; - @media (min-width: $screen-md-min) { - padding-left: $sidebar_width; + @media (max-width: $screen-sm-max) { + padding-left: 0; } .sidebar-wrapper { @@ -276,7 +211,7 @@ } .nav-sidebar li a { - width: 230px; + width: $sidebar_width; &.back-link { i { @@ -285,10 +220,6 @@ } } } - - .layout-nav { - padding-right: $sidebar_width; - } } .right-sidebar-collapsed { @@ -307,7 +238,9 @@ padding-right: 0; @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - padding-right: $sidebar_collapsed_width; + &:not(.build-sidebar) { + padding-right: $sidebar_collapsed_width; + } } @media (min-width: $screen-md-min) { diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 29501069d27..0b0bd80c326 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -5,7 +5,7 @@ padding: 0; .timeline-entry { - padding: $gl-padding $gl-btn-padding; + padding: $gl-padding $gl-btn-padding 11px; border-color: $table-border-color; color: $gl-gray; border-bottom: 1px solid $border-white-light; diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 6a45c34ccbb..e3154657c54 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -192,3 +192,8 @@ .text-info:hover { color: $brand-info; } + +// Prevent datetimes on tooltips to break into two lines +.local-timeago { + white-space: nowrap; +} diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 2779cd56788..3575984b229 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -269,3 +269,11 @@ h1, h2, h3, h4 { text-align: right; } } + +.idiff.deletion { + background: $line-removed-dark; +} + +.idiff.addition { + background: $line-added-dark; +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index ccb4e5381b7..752d8ec8788 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -2,7 +2,7 @@ * Layout */ $sidebar_collapsed_width: 62px; -$sidebar_width: 230px; +$sidebar_width: 220px; $gutter_collapsed_width: 62px; $gutter_width: 290px; $gutter_inner_width: 258px; @@ -12,7 +12,7 @@ $gutter_inner_width: 258px; */ $border-color: #e5e5e5; $focus-border-color: #3aabf0; -$table-border-color: #ececec; +$table-border-color: #f0f0f0; $background-color: #fafafa; /* @@ -57,13 +57,15 @@ $code_line_height: 1.5; */ $gl-padding: 16px; $gl-btn-padding: 10px; +$gl-input-padding: 10px; $gl-vert-padding: 6px; $gl-padding-top: 10px; /* * Misc */ -$row-hover: #f4f8fe; +$row-hover: #f7faff; +$row-hover-border: #b2d7ff; $progress-color: #c0392b; $avatar_radius: 50%; $header-height: 50px; @@ -78,6 +80,9 @@ $provider-btn-not-active-color: #4688f1; $link-underline-blue: #4a8bee; $layout-link-gray: #7e7c7c; $todo-alert-blue: #428bca; +$btn-side-margin: 10px; +$btn-sm-side-margin: 7px; +$btn-xs-side-margin: 5px; /* * Color schema @@ -104,7 +109,7 @@ $blue-medium-light: #3498cb; $blue-medium: #2f8ebf; $blue-medium-dark: #2d86b4; -$orange-light: rgba(252, 109, 38, 0.80); +$orange-light: #fc8a51; $orange-normal: #e75e40; $orange-dark: #ce5237; @@ -119,8 +124,8 @@ $border-white-light: #f1f2f4; $border-white-normal: #d6dae2; $border-white-dark: #c6cacf; -$border-gray-light: rgba(0, 0, 0, 0.06); -$border-gray-normal: rgba(0, 0, 0, 0.10);; +$border-gray-light: #dcdcdc; +$border-gray-normal: #d7d7d7; $border-gray-dark: #c6cacf; $border-green-light: #2faa60; @@ -178,6 +183,7 @@ $table-border-gray: #f0f0f0; $line-target-blue: #eaf3fc; $line-select-yellow: #fcf8e7; $line-select-yellow-dark: #f0e2bd; + /* * Fonts */ @@ -215,6 +221,7 @@ $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; $btn-active-gray: #ececec; $btn-placeholder-gray: #c7c7c7; $btn-white-active: #848484; +$btn-gray-hover: #eee; /* * Award emoji @@ -253,3 +260,6 @@ $calendar-header-color: #b8b8b8; $calendar-hover-bg: #ecf3fe; $calendar-border-color: rgba(#000, .1); $calendar-unselectable-bg: #faf9f9; + +$ci-output-bg: #1d1f21; +$ci-text-color: #c5c8c6; diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index f870ea0d87f..ff02ebdd34c 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -32,7 +32,7 @@ } } -.zen-cotrol { +.zen-control { padding: 0; color: #555; background: none; diff --git a/app/assets/stylesheets/mailers/devise.scss b/app/assets/stylesheets/mailers/devise.scss new file mode 100644 index 00000000000..28611a5ec81 --- /dev/null +++ b/app/assets/stylesheets/mailers/devise.scss @@ -0,0 +1,134 @@ +// NOTE: This stylesheet is for the exclusive use of the `devise_mailer` layout +// used for Devise email templates, and _should not_ be included in any +// application stylesheets. +// +// Styles defined here are embedded directly into the resulting email HTML via +// the `premailer` gem. + +$body-background-color: #363636; +$message-background-color: #fafafa; + +$header-color: #6b4fbb; +$body-color: #444; +$cta-color: #e14329; +$footer-link-color: #7e7e7e; + +$font-family: Helvetica, Arial, sans-serif; + +body { + background-color: $body-background-color; + font-family: $font-family; + margin: 0; + padding: 0; +} + +table { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + + border: 0; + border-collapse: separate; + + &#wrapper { + background-color: $body-background-color; + width: 100%; + } + + &#header { + margin: 0 auto; + text-align: left; + width: 600px; + } + + &#body { + background-color: $message-background-color; + border: 1px solid #000; + border-radius: 4px; + margin: 0 auto; + width: 600px; + } + + &#footer { + color: $footer-link-color; + font-size: 14px; + text-align: center; + width: 100%; + } + + td { + &#body-container { + padding: 20px 40px; + } + } +} + +.center { + text-align: center; +} + +#logo { + border: none; + outline: none; + min-height: 88px; + width: 134px; +} + +#content { + h2 { + color: $header-color; + font-size: 30px; + font-weight: 400; + line-height: 34px; + margin-top: 0; + } + + p { + color: $body-color; + font-size: 17px; + line-height: 24px; + margin-bottom: 0; + } +} + +#cta { + border: 1px solid $cta-color; + border-radius: 3px; + display: inline-block; + margin: 20px 0; + padding: 12px 24px; + + a { + background-color: $message-background-color; + color: $cta-color; + display: inline-block; + text-decoration: none; + } +} + +#tanuki { + padding: 40px 0 0; + + img { + border: none; + outline: none; + width: 37px; + min-height: 36px; + } +} + +#tagline { + font-size: 22px; + font-weight: 100; + padding: 4px 0 40px; +} + +#social { + padding: 0 10px 20px; + width: 600px; + word-spacing: 20px; + + a { + color: $footer-link-color; + text-decoration: none; + } +} diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss new file mode 100644 index 00000000000..7f645d3089d --- /dev/null +++ b/app/assets/stylesheets/mailers/repository_push_email.scss @@ -0,0 +1,182 @@ +@import "framework/variables"; + +// This file is largely copied from `highlight/white.scss`, but modified to +// avoid all descendant selectors (`table td`). This is because the CSS inlining +// we use performs dramatically worse on descendant selectors than the +// alternatives. +// <https://gitlab.com/gitlab-org/gitlab-ee/issues/490#note_12283632> +// +// DO NOT ADD ANY DESCENDANT SELECTORS TO THIS FILE. Instead, use (in order of +// preference): plain class selectors, type (element name) selectors, or +// explicit child selectors. + +table.code { + width: 100%; + font-family: monospace; + border: none; + border-collapse: separate; + margin: 0; + padding: 0; + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + + > tr > td { + line-height: $code_line_height; + font-family: monospace; + font-size: $code_font_size; + + &.diff-line-num { + margin: 0; + padding: 0; + border: none; + padding: 0 5px; + border-right: 1px solid; + text-align: right; + min-width: 35px; + max-width: 50px; + width: 35px; + } + + &.line_content { + display: block; + margin: 0; + padding: 0 0.5em; + border: none; + white-space: pre; + } + } +} + +.line-numbers, .diff-line-num { + background-color: $background-color; +} + +.diff-line-num, .diff-line-num a { + color: $black-transparent; +} + +pre.code, .diff-line-num { + border-color: $table-border-gray; +} + +.code.white, pre.code, .line_content { + background-color: #fff; + color: #333; +} + +.diff-line-num { + &.old { + background-color: $line-number-old; + border-color: $line-removed-dark; + } + + &.new { + background-color: $line-number-new; + border-color: $line-added-dark; + } + + &.hll:not(.empty-cell) { + background-color: $line-number-select; + border-color: $line-select-yellow-dark; + } +} + +.line_content { + &.old { + background-color: $line-removed; + + > .line > span.idiff, > .line > span > span.idiff { + background-color: $line-removed-dark; + } + } + + &.new { + background-color: $line-added; + + > .line > span.idiff, > .line > span > span.idiff { + background-color: $line-added-dark; + } + } + + &.match { + color: $black-transparent; + background-color: $match-line; + } + + &.hll:not(.empty-cell) { + background-color: $line-select-yellow; + } +} + +pre > .hll { + background-color: #f8eec7 !important; +} + +span.highlight_word { + background-color: #fafe3d !important; +} + +.hll { background-color: #f8f8f8 } +.c { color: #998; font-style: italic; } +.err { color: #a61717; background-color: #e3d2d2; } +.k { font-weight: bold; } +.o { font-weight: bold; } +.cm { color: #998; font-style: italic; } +.cp { color: #999; font-weight: bold; } +.c1 { color: #998; font-style: italic; } +.cs { color: #999; font-weight: bold; font-style: italic; } +.gd { color: #000; background-color: #fdd; } +.gd .x { color: #000; background-color: #faa; } +.ge { font-style: italic; } +.gr { color: #a00; } +.gh { color: #999; } +.gi { color: #000; background-color: #dfd; } +.gi .x { color: #000; background-color: #afa; } +.go { color: #888; } +.gp { color: #555; } +.gs { font-weight: bold; } +.gu { color: #800080; font-weight: bold; } +.gt { color: #a00; } +.kc { font-weight: bold; } +.kd { font-weight: bold; } +.kn { font-weight: bold; } +.kp { font-weight: bold; } +.kr { font-weight: bold; } +.kt { color: #458; font-weight: bold; } +.m { color: #099; } +.s { color: #d14; } +.n { color: #333; } +.na { color: teal; } +.nb { color: #0086b3; } +.nc { color: #458; font-weight: bold; } +.no { color: teal; } +.ni { color: purple; } +.ne { color: #900; font-weight: bold; } +.nf { color: #900; font-weight: bold; } +.nn { color: #555; } +.nt { color: navy; } +.nv { color: teal; } +.ow { font-weight: bold; } +.w { color: #bbb; } +.mf { color: #099; } +.mh { color: #099; } +.mi { color: #099; } +.mo { color: #099; } +.sb { color: #d14; } +.sc { color: #d14; } +.sd { color: #d14; } +.s2 { color: #d14; } +.se { color: #d14; } +.sh { color: #d14; } +.si { color: #d14; } +.sx { color: #d14; } +.sr { color: #009926; } +.s1 { color: #d14; } +.ss { color: #990073; } +.bp { color: #999; } +.vc { color: teal; } +.vg { color: teal; } +.vi { color: teal; } +.il { color: #099; } +.gc { color: #999; background-color: #eaf2f5; } diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss index 0a13a7e0b54..fc12964872d 100644 --- a/app/assets/stylesheets/notify.scss +++ b/app/assets/stylesheets/notify.scss @@ -6,19 +6,19 @@ p.details { font-style: italic; color: #777 } -.footer p { +.footer > p { font-size: small; color: #777 } pre.commit-message { white-space: pre-wrap; } -.file-stats a { +.file-stats > a { text-decoration: none; -} -.file-stats .new-file { - color: #090; -} -.file-stats .deleted-file { - color: #b00; + > .new-file { + color: #090; + } + > .deleted-file { + color: #b00; + } } diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss index 37bf38fa65d..6211f3a52eb 100644 --- a/app/assets/stylesheets/pages/awards.scss +++ b/app/assets/stylesheets/pages/awards.scss @@ -1,6 +1,4 @@ .awards { - line-height: 34px; - .emoji-icon { width: 20px; height: 20px; @@ -9,8 +7,6 @@ .emoji-menu { position: absolute; - top: 100%; - left: 0; margin-top: 3px; z-index: 1000; min-width: 160px; @@ -23,7 +19,12 @@ opacity: 0; transform: scale(.2); transform-origin: 0 -45px; - transition: all .3s cubic-bezier(.87,-.41,.19,1.44); + transition: .3s cubic-bezier(.87,-.41,.19,1.44); + transition-property: transform, opacity; + + &.is-aligned-right { + transform-origin: 100% -45px; + } &.is-visible { pointer-events: all; @@ -94,20 +95,30 @@ .award-control { margin-right: 5px; + margin-bottom: 5px; padding-left: 5px; padding-right: 5px; line-height: 20px; outline: 0; + &:hover, &.active, &:active { - background-color: $white-dark; + background-color: $row-hover; + border-color: $row-hover-border; box-shadow: none; outline: 0; } + &.btn { + &:focus { + outline: 0; + } + } + &.is-loading { - .award-control-icon { + .award-control-icon-normal, + .emoji-icon { display: none; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index aa41565f812..e8f1935d239 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -3,12 +3,7 @@ background: #111; color: #fff; font-family: $monospace_font; - white-space: pre; - white-space: pre-wrap; /* css-3 */ - white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ - white-space: -pre-wrap; /* Opera 4-6 */ - white-space: -o-pre-wrap; /* Opera 7 */ - word-wrap: break-word; /* Internet Explorer 5.5+ */ + white-space: pre-wrap; overflow: auto; overflow-y: hidden; font-size: 12px; @@ -58,37 +53,92 @@ left: 70px; } } +} - .build-widget { - padding: 10px; - background: $background-color; - margin-bottom: 20px; - border-radius: 4px; +.build-header { + position: relative; + padding-right: 40px; - .title { - margin-top: 0; - color: #666; - line-height: 1.5; - } - .attr-name { - color: #777; - } + @media (min-width: $screen-sm-min) { + padding-right: 0; } - .alert-disabled { - background: $background-color; + a { + color: $gl-gray; - a { - color: #3084bb !important; + &:hover { + color: $gl-link-color; + text-decoration: none; } } + + code { + color: $code-color; + } + + .avatar { + float: none; + margin-right: 2px; + margin-left: 2px; + } } table.builds { - .build-link { a { color: $gl-dark-link-color; } } } + +.build-trace { + background: $ci-output-bg; + color: $ci-text-color; + white-space: pre; + overflow-x: auto; + font-size: 12px; + + .fa-refresh { + font-size: 24px; + } + + .bash { + display: block; + } +} + +.right-sidebar.build-sidebar { + padding-top: $gl-padding; + padding-bottom: $gl-padding; + + &.right-sidebar-collapsed { + display: none; + } + + .block { + width: 100%; + } + + .build-sidebar-header { + padding-top: 0; + + .gutter-toggle { + margin-top: 0; + } + } +} + +.build-detail-row { + margin-bottom: 5px; +} + +.build-light-text { + color: $gl-placeholder-color; +} + +.build-gutter-toggle { + position: absolute; + top: 50%; + right: 0; + margin-top: -17px; +} diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index c2cd227571f..fc3f214aba5 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -26,8 +26,28 @@ .commit-info-row { margin-bottom: 10px; + + &.commit-info-row-header { + line-height: 34px; + + @media (min-width: $screen-sm-min) { + margin-bottom: 0; + } + + .commit-options-dropdown-caret { + @media (max-width: $screen-sm) { + margin-left: 0; + } + } + } + .avatar { @extend .avatar-inline; + margin-left: 0; + + @media (min-width: $screen-sm-min) { + margin-left: 4px; + } } .commit-committer-link, .commit-author-link { @@ -35,10 +55,6 @@ font-weight: bold; } - .time_ago { - margin-left: 8px; - } - .fa-clipboard { color: $dropdown-title-btn-color; } diff --git a/app/assets/stylesheets/pages/confirmation.scss b/app/assets/stylesheets/pages/confirmation.scss index 125f495d6d4..292225c5261 100644 --- a/app/assets/stylesheets/pages/confirmation.scss +++ b/app/assets/stylesheets/pages/confirmation.scss @@ -2,13 +2,21 @@ margin-bottom: 20px; border-bottom: 1px solid #eee; - > h1 { + > h1, h2, h3, h4, h5, h6 { font-weight: 400; } .lead { margin-bottom: 20px; } + + ul, ol { + padding-left: 0; + } + + li { + list-style-type: none; + } } .confirmation-content { diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 5e61e61d85c..1b389d83525 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -29,8 +29,6 @@ margin-top: 6px; p { - overflow-x: auto; - &:last-child { margin-bottom: 0; } diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 8981f070a20..22679c764dc 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -23,7 +23,7 @@ .file-title { @extend .monospace; - line-height: 42px; + line-height: 35px; padding-top: 7px; padding-bottom: 7px; @@ -43,7 +43,7 @@ .editor-file-name { @extend .monospace; - + float: left; margin-right: 10px; } @@ -59,7 +59,22 @@ } .encoding-selector, - .license-selector { + .license-selector, + .gitignore-selector { display: inline-block; + vertical-align: top; + font-family: $regular_font; + } + + .gitignore-selector { + + .dropdown { + line-height: 21px; + } + + .dropdown-menu-toggle { + vertical-align: top; + width: 220px; + } } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 1cf3023ecc9..ea453ce356a 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -29,7 +29,7 @@ } } -.issuable-sidebar { +.right-sidebar { a { color: inherit; } @@ -74,6 +74,10 @@ } } + .block-first { + padding-top: 0; + } + .title { color: $gl-text-color; margin-bottom: 10px; @@ -125,7 +129,7 @@ .right-sidebar { position: fixed; - top: 58px; + top: $header-height; bottom: 0; right: 0; z-index: 10; @@ -150,6 +154,10 @@ font-weight: 600; } + .light { + font-weight: normal; + } + .sidebar-collapsed-icon { display: none; } diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index fc9db97132d..4e35ca329e4 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -40,11 +40,6 @@ } } -.issue-search-form { - margin: 0; - height: 24px; -} - form.edit-issue { margin: 0; } @@ -96,8 +91,3 @@ form.edit-issue { .issue-form .select2-container { width: 250px !important; } - -.issue-closed-by-widget { - color: $gl-text-color; - margin-left: 52px; -} diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index e179bdf0048..bc65404a741 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -50,11 +50,26 @@ .label-row { .label-name { - display: inline-block; - width: 200px; + display: block; + margin-bottom: 10px; - @media (max-width: $screen-xs-min) { - display: block; + @media (min-width: $screen-sm-min) { + display: inline-block; + width: 200px; + margin-bottom: 0; + } + } + + .label-description { + display: block; + margin-bottom: 10px; + + @media (min-width: $screen-sm-min) { + display: inline-block; + width: 40%; + margin-left: 10px; + margin-bottom: 0; + vertical-align: middle; } } @@ -68,10 +83,6 @@ padding: 3px 4px; } -.label-subscription { - display: inline-block; -} - .dropdown-labels-error { padding: 5px 10px; margin-bottom: 10px; @@ -79,62 +90,95 @@ color: $white-light; } -@mixin labels-mobile { - @media (max-width: $screen-xs-min) { - display: block; - width: 100%; - margin-left: 0; - padding: 10px 0; - } -} +.manage-labels-list { + .btn-action { + color: $gl-dark-link-color; + .fa { + font-size: 18px; + vertical-align: middle; + } -.manage-labels-list { + &:hover { + color: $gl-link-color; - .prepend-left-10, .prepend-description-left { - display: inline-block; - width: 40%; - vertical-align: middle; + &.remove-row { + color: $gl-danger; + } + } + } - @include labels-mobile; + .dropdown { + @media (min-width: $screen-sm-min) { + float: right; + } } +} - .prepend-description-left { - width: 57%; +.prioritized-labels { + margin-bottom: 30px; - @include labels-mobile; + .add-priority { + display: none; + color: $gray-light; } +} - .pull-info-right { - float: right; +.other-labels { + .remove-priority { + display: none; + } +} - @media (max-width: $screen-xs-min) { - float: none; - } +.toggle-priority { + display: inline-block; + vertical-align: middle; - .action-buttons { - border-color: transparent; - padding: 6px; - color: $gl-text-color; + button { + border-color: transparent; + padding: 5px 8px; + vertical-align: top; + font-size: 14px; - &.label-subscribe-button { - padding-left: 0; - } + &:hover { + border-color: transparent; } + } +} - i { - color: $gl-text-color; +.filtered-labels { + .label-row { + &:not(:last-child) { + margin-right: 5px; } + } - .append-right-20 { - a { - color: $gl-text-color; - } + .label-remove { + border-left: 1px solid rgba(0, 0, 0, .1); + z-index: 3; + } - @media (max-width: $screen-xs-min) { - display: block; - margin-bottom: 10px; - } + .btn { + color: inherit; + } +} + +.label-options-toggle { + width: 100%; +} + +.label-subscribe-button { + .label-subscribe-button-loading { + display: none; + } + + &.disabled { + .label-subscribe-button-icon { + display: none; + } + + .label-subscribe-button-loading { + display: block; } } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index c4005ba1e69..a47f2580aa3 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -41,7 +41,7 @@ margin: 0; margin-left: 20px; padding: 5px; - padding-top: 12px; + padding-top: 8px; line-height: 20px; &.right { @@ -79,11 +79,14 @@ } &.ci-failed, - &.ci-canceled, &.ci-error { color: $gl-danger; } + &.ci-canceled { + color: $gl-gray; + } + a.monospace { color: inherit; } @@ -105,11 +108,39 @@ font-size: 17px; margin: 5px 0; color: $gl-gray-dark; + + &.has-conflicts .fa-exclamation-triangle { + color: $gl-warning; + } + } p:last-child { margin-bottom: 0; } + + @media (max-width: $screen-sm-max) { + h4 { + font-size: 15px; + } + + p { + font-size: 13px; + } + + .btn, + .btn-group, + .accept-action { + width: 100%; + margin-bottom: 4px; + } + + .accept-control { + width: 100%; + text-align: center; + margin: 0; + } + } } .mr-widget-footer { @@ -280,11 +311,5 @@ background-color: $white-light; color: $gl-placeholder-color; } - - th, - td { - padding: 16px; - } } } - diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 7fa13e66b43..577dddae741 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -87,6 +87,39 @@ } } +.md-header .nav-links { + display: flex; + display: -webkit-flex; + flex-flow: row wrap; + -webkit-flex-flow: row wrap; + width: 100%; + + .pull-right { + // Flexbox quirk to make sure right-aligned items stay right-aligned. + margin-left: auto; + } +} + +.confidential-issue-warning { + background-color: $gray-normal; + border-radius: 3px; + padding: 3px 12px; + margin: auto; + margin-top: 0; + text-align: center; + font-size: 13px; + + @media (max-width: $screen-md-min) { + // On smaller devices the warning becomes the fourth item in the list, + // rather than centering, and grows to span the full width of the + // comment area. + order: 4; + -webkit-order: 4; + margin: 6px auto; + width: 100%; + } +} + .discussion-form { padding: $gl-padding-top $gl-padding; background-color: $white-light; @@ -96,17 +129,8 @@ display: none; font-size: 15px; - .form-actions { - padding-left: 20px; - - .btn-save { - float: left; - } - - .note-form-option { - float: left; - padding: 2px 0 0 25px; - } + .md-area { + background-color: #fff; } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 624c8249f7e..0c084118753 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -69,6 +69,10 @@ ul.notes { .note-edit-form { display: block; + + &.current-note-edit-form + .note-awards { + display: none; + } } } @@ -116,8 +120,41 @@ ul.notes { } } + .note-awards { + .js-awards-block { + padding: 2px; + margin-top: 10px; + } + + .award-control { + font-size: 13px; + padding: 2px 5px; + } + } + .note-header { padding-bottom: 3px; + padding-right: 20px; + + @media (min-width: $screen-sm-min) { + padding-right: 0; + } + } + + .note-emoji-button { + .fa-spinner { + display: none; + } + + &.is-loading { + .fa-smile-o { + display: none; + } + + .fa-spinner { + display: inline-block; + } + } } } @@ -179,6 +216,8 @@ ul.notes { .discussion-header, .note-header { + position: relative; + a { color: inherit; @@ -215,6 +254,16 @@ ul.notes { color: $notes-action-color; } +.note-actions { + position: absolute; + right: 0; + top: 0; + + @media (min-width: $screen-sm-min) { + position: relative; + } +} + .discussion-actions { @media (max-width: $screen-md-max) { float: none; @@ -226,11 +275,15 @@ ul.notes { } } -.note-action-button, -.discussion-action-button { +.note-action-button { display: inline-block; - margin-left: 10px; - line-height: 24px; + margin-left: 0; + line-height: 20px; + + @media (min-width: $screen-sm-min) { + margin-left: 10px; + line-height: 24px; + } .fa { color: $notes-action-color; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss new file mode 100644 index 00000000000..6128868b670 --- /dev/null +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -0,0 +1,24 @@ +.pipelines { + .stage { + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .duration, .finished_at { + margin: 4px 0; + } + + .commit-title { + margin: 0; + } + + .controls { + white-space: nowrap; + } + + .btn { + margin: 4px; + } +} diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index abc5a0e9877..167ab40d881 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -66,12 +66,6 @@ } } -.calendar-hint { - margin-top: -12px; - float: right; - font-size: 12px; -} - .profile-link-holder { display: inline; @@ -134,14 +128,6 @@ } } -.change-username-title { - color: $gl-warning; -} - -.remove-account-title { - color: $gl-danger; -} - .provider-btn-group { display: inline-block; margin-right: 10px; @@ -218,7 +204,7 @@ .btn { display: inline-block; - width: 48%; + width: 46%; } } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index c20f04653fc..bb250904255 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -7,10 +7,10 @@ } .no-ssh-key-message, .project-limit-message { background-color: #f28d35; - margin-bottom: 16px; + margin-bottom: 0; } .new_project, -.edit_project { +.edit-project { fieldset.features { .control-label { font-weight: normal; @@ -26,8 +26,22 @@ } .project-home-panel { - padding-bottom: 40px; - border-bottom: 1px solid $border-color; + background: $white-light; + text-align: left; + padding: 24px 0; + + .container-fluid { + position: relative; + + @media (min-width: $screen-md-max) { + .row { + display: flex; + -ms-flex-align: center; + -webkit-align-items: center; + -webkit-box-align: center; + } + } + } .cover-controls { .project-settings-dropdown { @@ -43,21 +57,54 @@ } } - .project-identicon-holder { - margin-bottom: 16px; + .cover-title { + margin-bottom: 0; + } + + .project-image-container { + @include make-sm-column(1); + max-width: 86px; + min-width: 86px; + padding-right: 0; - .avatar, .identicon { - margin: 0 auto; - float: none; + @media (max-width: $screen-md-max) { + padding-left: 0; + margin: 0 0 10px; + max-width: none; + min-width: none; + + .avatar.s70 { + margin: auto; + } } + } - .identicon { - @include border-radius(50%); + .project-info { + @include make-sm-column(10); + + h1 { + font-size: 24px; + font-weight: normal; + margin: 0; } + + .project-home-desc { + p { + margin: 0; + } + } + } + + .identicon { + float: left; + @include border-radius(50%); + } + + .avatar { + float: none; } .notifications-btn { - margin-top: -28px; .fa-bell { margin-right: 6px; @@ -69,28 +116,45 @@ } .project-repo-buttons { - margin-top: 20px; - margin-bottom: 0; + font-size: 0; - .count-buttons { - display: block; - margin-bottom: 20px; - } + .btn { + @include btn-gray; + padding: 3px 10px; + text-transform: none; + background-color: $background-color; - .clone-row { - .split-repo-buttons, - .project-clone-holder { - display: inline-block; + .fa { + color: $layout-link-gray; } - .split-repo-buttons { - margin: 0 12px; + .fa-caret-down { + margin-left: 3px; } } - .btn { - @include btn-gray; - text-transform: none; + .btn-group:not(:first-child):not(:last-child) > .btn { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + } + + form { + margin-left: 10px; + } + + .count-buttons { + display: inline-block; + vertical-align: top; + margin-top: 16px; + } + + .project-clone-holder { + display: inline-block; + margin-top: 16px; + + input { + height: 29px; + } } .count-with-arrow { @@ -140,14 +204,18 @@ line-height: 13px; padding: $gl-vert-padding $gl-padding; letter-spacing: .4px; - padding: 10px 14px; + padding: 7px 14px; text-align: center; vertical-align: middle; touch-action: manipulation; cursor: pointer; background-image: none; white-space: nowrap; - margin: 0 11px 0 4px; + margin: 0 10px 0 4px; + + a { + color: inherit; + } &:hover { background: #fff; @@ -155,13 +223,37 @@ } } } + + .project-right-buttons { + position: absolute; + right: 16px; + bottom: 0; + + .btn { + padding: 3px 10px; + background-color: $background-color; + } + + @media (max-width: 1304px) { + top: 0; + } + } + + @media (max-width: $screen-md-max) { + text-align: center; + + .project-info, + .project-image-container { + width: 100%; + } + } } .split-one { display: inline-table; margin-right: 12px; - a { + > a { margin: -1px; } } @@ -285,11 +377,11 @@ a.deploy-project-label { } .project-stats { - text-align: center; margin-top: $gl-padding; margin-bottom: 0; - padding-top: 10px; - padding-bottom: 4px; + padding: 16px 0; + background-color: $white-light; + font-size: 0; ul.nav { display: inline-block; @@ -300,12 +392,11 @@ a.deploy-project-label { } .nav > li > a { - @include btn-default; - @include btn-gray; - background-color: transparent; - border: 1px solid #f7f8fa; - margin-left: 12px; + margin-right: 12px; + padding: 0 10px; + font-size: 15px; + color: $notes-light-color; } li { @@ -325,6 +416,10 @@ a.deploy-project-label { background-color: #f0f2f5; } } + + &.row-content-block.second-block { + margin-top: 0; + } } pre.light-well { @@ -402,9 +497,11 @@ pre.light-well { margin: 0; } -.project-show-activity { - .activity-filter-block { - margin-top: -1px; + +.activity-filter-block { + .controls { + padding-bottom: 10px; + border-bottom: 1px solid $border-color; } } @@ -442,9 +539,14 @@ pre.light-well { border-top: 0; .edit-project-readme { - z-index: 100; + z-index: 2; position: relative; } + + .wiki h1 { + border-bottom: none; + padding: 0; + } } .git-clone-holder { diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 2bff70c8c64..ae524cd6bae 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -28,6 +28,7 @@ } .search-input { + padding-right: 20px; border: none; font-size: 14px; outline: none; @@ -47,6 +48,7 @@ display: inline-block; background-color: $location-badge-bg; vertical-align: top; + cursor: default; } .search-input-container { @@ -55,7 +57,7 @@ position: relative; } - .search-location-badge, .search-input-wrap { + .search-input-wrap { // Fallback if flexbox is not supported display: inline-block; } @@ -156,13 +158,11 @@ .search-holder { @media (min-width: $screen-sm-min) { display: -webkit-flex; - display: -ms-flexbox; display: flex; } .search-field-holder { -webkit-flex: 1 0 auto; - -ms-flex: 1 0 auto; flex: 1 0 auto; position: relative; margin-right: 0; diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 3fb70085713..2e8f356298d 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -12,3 +12,11 @@ border: 1px solid $warning-message-border; border-radius: $border-radius-base; } + +.warning-title { + color: $gl-warning; +} + +.danger-title { + color: $gl-danger; +} diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index 639d639d5b0..2aa939b7dc3 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -16,19 +16,6 @@ } } -.snippet-box { - @include border-radius(2px); - - display: block; - float: left; - padding: 0 $gl-padding; - font-weight: normal; - margin-right: 10px; - font-size: $gl-font-size; - border: 1px solid; - line-height: 32px; -} - .markdown-snippet-copy { position: fixed; top: -10px; @@ -36,3 +23,34 @@ max-height: 0; max-width: 0; } + +.file-holder.snippet-file-content { + padding-bottom: $gl-padding; + border-bottom: 1px solid $border-color; + + .file-title { + padding-top: $gl-padding; + padding-bottom: $gl-padding; + } + + .file-actions { + top: 12px; + } + + .file-content { + border-left: 1px solid $border-color; + border-right: 1px solid $border-color; + border-bottom: 1px solid $border-color; + } +} + +.snippet-title { + font-size: 24px; + font-weight: normal; +} + +.snippet-actions { + @media (min-width: $screen-sm-min) { + float: right; + } +} diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index e51c3491dae..afc00a68572 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -29,6 +29,17 @@ .todo-item { .todo-title { @include str-truncated(calc(100% - 174px)); + overflow: visible; + } + + .status-box { + margin: 0; + float: none; + display: inline-block; + font-weight: normal; + padding: 0 5px; + line-height: inherit; + font-size: 14px; } .todo-body { @@ -76,12 +87,11 @@ @media (max-width: $screen-xs-max) { .todo-item { - padding-left: $gl-padding; - .todo-title { white-space: normal; overflow: visible; max-width: 100%; + margin-bottom: 10px; } .avatar { diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index a84fc2e0318..f16fc7f388f 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -15,16 +15,23 @@ margin-bottom: 0; tr { - > td, > th { + border-bottom: 1px solid $table-border-gray; + border-top: 1px solid $table-border-gray; + + td, th { line-height: 23px; } &:hover { + cursor: pointer; + td { - background: $row-hover; + background-color: $row-hover; + border-top: 1px solid $row-hover-border; + border-bottom: 1px solid $row-hover-border; } - cursor: pointer; } + &.selected { td { background: $gray-dark; diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss index 3f28e402929..8d855ce99b0 100644 --- a/app/assets/stylesheets/pages/xterm.scss +++ b/app/assets/stylesheets/pages/xterm.scss @@ -11,18 +11,15 @@ $magenta: #cd00cd; $cyan: #00cdcd; $white: #e5e5e5; - $l-black: #7f7f7f; - $l-red: #f00; - $l-green: #0f0; - $l-yellow: #ff0; - $l-blue: #5c5cff; - $l-magenta: #f0f; - $l-cyan: #0ff; - $l-white: #fff; + $l-black: #373b41; + $l-red: #c66; + $l-green: #b5bd68; + $l-yellow: #f0c674; + $l-blue: #81a2be; + $l-magenta: #b294bb; + $l-cyan: #8abeb7; + $l-white: $ci-text-color; - .term-bold { - font-weight: bold; - } .term-italic { font-style: italic; } diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb index e9b0972bdd8..5055c318a5f 100644 --- a/app/controllers/admin/abuse_reports_controller.rb +++ b/app/controllers/admin/abuse_reports_controller.rb @@ -9,6 +9,6 @@ class Admin::AbuseReportsController < Admin::ApplicationController abuse_report.remove_user(deleted_by: current_user) if params[:remove_user] abuse_report.destroy - render nothing: true + head :ok end end diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index ec22548ddeb..f4eda864aac 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -19,6 +19,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController redirect_to admin_runners_path end + def reset_health_check_token + @application_setting.reset_health_check_access_token! + flash[:notice] = 'New health check access token has been generated!' + redirect_to :back + end + def clear_repository_check_states RepositoryCheck::ClearWorker.perform_async @@ -53,6 +59,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end end + enabled_oauth_sign_in_sources = params[:application_setting].delete(:enabled_oauth_sign_in_sources) + + params[:application_setting][:disabled_oauth_sign_in_sources] = + AuthHelper.button_based_providers.map(&:to_s) - + Array(enabled_oauth_sign_in_sources) + params.require(:application_setting).permit( :default_projects_limit, :default_branch_protection, @@ -62,6 +74,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :two_factor_grace_period, :gravatar_enabled, :sign_in_text, + :after_sign_up_text, :help_page_text, :home_page_url, :after_sign_out_path, @@ -94,8 +107,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :email_author_in_body, :repository_checks_enabled, :metrics_packet_size, + :send_user_confirmation_email, + :container_registry_token_expire_delay, restricted_visibility_levels: [], - import_sources: [] + import_sources: [], + disabled_oauth_sign_in_sources: [] ) end end diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index fc342924987..82055006ac0 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -32,7 +32,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController respond_to do |format| format.html { redirect_back_or_default(default: { action: 'index' }) } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb new file mode 100644 index 00000000000..241c7be0ea1 --- /dev/null +++ b/app/controllers/admin/health_check_controller.rb @@ -0,0 +1,5 @@ +class Admin::HealthCheckController < Admin::ApplicationController + def show + @errors = HealthCheck::Utils.process_checks('standard') + end +end diff --git a/app/controllers/admin/keys_controller.rb b/app/controllers/admin/keys_controller.rb index cb33fdd9763..054bb52b696 100644 --- a/app/controllers/admin/keys_controller.rb +++ b/app/controllers/admin/keys_controller.rb @@ -6,7 +6,7 @@ class Admin::KeysController < Admin::ApplicationController respond_to do |format| format.html - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index a701d49b844..7345c91f67d 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -9,23 +9,18 @@ class Admin::RunnersController < Admin::ApplicationController end def show - @builds = @runner.builds.order('id DESC').first(30) - @projects = - if params[:search].present? - ::Project.search(params[:search]) - else - Project.all - end - @projects = @projects.where.not(id: @runner.projects.select(:id)) if @runner.projects.any? - @projects = @projects.page(params[:page]).per(30) + assign_builds_and_projects end def update - @runner.update_attributes(runner_params) - - respond_to do |format| - format.js - format.html { redirect_to admin_runner_path(@runner) } + if @runner.update_attributes(runner_params) + respond_to do |format| + format.js + format.html { redirect_to admin_runner_path(@runner) } + end + else + assign_builds_and_projects + render 'show' end end @@ -58,6 +53,18 @@ class Admin::RunnersController < Admin::ApplicationController end def runner_params - params.require(:runner).permit(:token, :description, :tag_list, :active) + params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) + end + + def assign_builds_and_projects + @builds = runner.builds.order('id DESC').first(30) + @projects = + if params[:search].present? + ::Project.search(params[:search]) + else + Project.all + end + @projects = @projects.where.not(id: runner.projects.select(:id)) if runner.projects.any? + @projects = @projects.page(params[:page]).per(30) end end diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb index 377e9741e5f..3a2f0185315 100644 --- a/app/controllers/admin/spam_logs_controller.rb +++ b/app/controllers/admin/spam_logs_controller.rb @@ -11,7 +11,7 @@ class Admin::SpamLogsController < Admin::ApplicationController redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed." else spam_log.destroy - render nothing: true + head :ok end end end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index f2f654c7bcd..f35f4a8c811 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -119,6 +119,7 @@ class Admin::UsersController < Admin::ApplicationController user_params_with_pass.merge!( password: params[:user][:password], password_confirmation: params[:user][:password_confirmation], + password_expires_at: Time.now ) end @@ -153,7 +154,7 @@ class Admin::UsersController < Admin::ApplicationController respond_to do |format| format.html { redirect_back_or_admin_user(notice: "Successfully removed email.") } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 17b3f49aed1..cd6ae507cf1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base include Gitlab::GonHelper include GitlabRoutingHelper include PageLayoutHelper + include WorkhorseHelper before_action :authenticate_user_from_token! before_action :authenticate_user! @@ -176,14 +177,14 @@ class ApplicationController < ActionController::Base end def check_password_expiration - if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user? + if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user? redirect_to new_profile_password_path and return end end def check_2fa_requirement - if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled && !skip_two_factor? - redirect_to new_profile_two_factor_auth_path + if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled? && !skip_two_factor? + redirect_to profile_two_factor_auth_path end end @@ -232,7 +233,7 @@ class ApplicationController < ActionController::Base end def configure_permitted_parameters - devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:username, :email, :password, :login, :remember_me, :otp_attempt) } + devise_parameter_sanitizer.permit(:sign_in, keys: [:username, :email, :password, :login, :remember_me, :otp_attempt]) end def hexdigest(string) @@ -263,7 +264,7 @@ class ApplicationController < ActionController::Base # internal repos where you are not a member. Enable this filter # or improve current implementation to filter only issues you # created or assigned or mentioned - #@filter_params[:authorized_only] = true + # @filter_params[:authorized_only] = true end @filter_params @@ -342,6 +343,10 @@ class ApplicationController < ActionController::Base session[:skip_tfa] && session[:skip_tfa] > Time.current end + def browser_supports_u2f? + browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile? + end + def redirect_to_home_page_url? # If user is not signed-in and tries to access root_path - redirect him to landing page # Don't redirect to the default URL to prevent endless redirections @@ -355,6 +360,13 @@ class ApplicationController < ActionController::Base current_user.nil? && root_path == request.path end + # U2F (universal 2nd factor) devices need a unique identifier for the application + # to perform authentication. + # https://developers.yubico.com/U2F/App_ID.html + def u2f_app_id + request.base_url + end + private def set_default_sort diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index eb0abc80ab4..3865b2d61fd 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -31,6 +31,24 @@ class AutocompleteController < ApplicationController render json: @user, only: [:name, :username, :id], methods: [:avatar_url] end + def projects + project = Project.find_by_id(params[:project_id]) + + projects = current_user.authorized_projects + projects = projects.select do |project| + current_user.can?(:admin_issue, project) + end + + no_project = { + id: 0, + name_with_namespace: 'No project', + } + projects.unshift(no_project) + projects.delete(project) + + render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace) + end + private def find_users diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index d5918a7af3b..998b8adc411 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -24,7 +24,64 @@ module AuthenticatesWithTwoFactor # Returns nil def prompt_for_two_factor(user) session[:otp_user_id] = user.id + setup_u2f_authentication(user) + render 'devise/sessions/two_factor' + end + + def authenticate_with_two_factor + user = self.resource = find_user + + if user_params[:otp_attempt].present? && session[:otp_user_id] + authenticate_with_two_factor_via_otp(user) + elsif user_params[:device_response].present? && session[:otp_user_id] + authenticate_with_two_factor_via_u2f(user) + elsif user && user.valid_password?(user_params[:password]) + prompt_for_two_factor(user) + end + end + + private + + def authenticate_with_two_factor_via_otp(user) + if valid_otp_attempt?(user) + # Remove any lingering user data from login + session.delete(:otp_user_id) + + remember_me(user) if user_params[:remember_me] == '1' + sign_in(user) + else + flash.now[:alert] = 'Invalid two-factor code.' + render :two_factor + end + end + + # Authenticate using the response from a U2F (universal 2nd factor) device + def authenticate_with_two_factor_via_u2f(user) + if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenges]) + # Remove any lingering user data from login + session.delete(:otp_user_id) + session.delete(:challenges) + + sign_in(user) + else + flash.now[:alert] = 'Authentication via U2F device failed.' + prompt_for_two_factor(user) + end + end + + # Setup in preparation of communication with a U2F (universal 2nd factor) device + # Actual communication is performed using a Javascript API + def setup_u2f_authentication(user) + key_handles = user.u2f_registrations.pluck(:key_handle) + u2f = U2F::U2F.new(u2f_app_id) - render 'devise/sessions/two_factor' and return + if key_handles.present? + sign_requests = u2f.authentication_requests(key_handles) + challenges = sign_requests.map(&:challenge) + session[:challenges] = challenges + gon.push(u2f: { challenges: challenges, app_id: u2f_app_id, + sign_requests: sign_requests, + browser_supports_u2f: browser_supports_u2f? }) + end end end diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 787416c17ab..dacb5679dd3 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -122,7 +122,7 @@ module CreatesCommit # Merge request from fork to this project @mr_source_project = @tree_edit_project @mr_target_project = @project - @mr_target_branch ||= @ref + @mr_target_branch ||= @ref end end end diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb new file mode 100644 index 00000000000..036777c80c1 --- /dev/null +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -0,0 +1,31 @@ +module ToggleAwardEmoji + extend ActiveSupport::Concern + + included do + before_action :authenticate_user!, only: [:toggle_award_emoji] + end + + def toggle_award_emoji + name = params.require(:name) + + awardable.toggle_award_emoji(name, current_user) + TodoService.new.new_award_emoji(to_todoable(awardable), current_user) + + render json: { ok: true } + end + + private + + def to_todoable(awardable) + case awardable + when Note + awardable.noteable + else + awardable + end + end + + def awardable + raise NotImplementedError + end +end diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb index 8a43c0b93c4..9e3b9be2ff4 100644 --- a/app/controllers/concerns/toggle_subscription_action.rb +++ b/app/controllers/concerns/toggle_subscription_action.rb @@ -6,7 +6,7 @@ module ToggleSubscriptionAction subscribable_resource.toggle_subscription(current_user) - render nothing: true + head :ok end private diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb index 23a4ef21ea2..2a88350a4ca 100644 --- a/app/controllers/dashboard/labels_controller.rb +++ b/app/controllers/dashboard/labels_controller.rb @@ -1,6 +1,6 @@ class Dashboard::LabelsController < Dashboard::ApplicationController def index - labels = Label.where(project_id: projects).select(:title, :color).uniq(:title) + labels = Label.where(project_id: projects).select(:id, :title, :color).uniq(:title) respond_to do |format| format.json { render json: labels } diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 71acc244a91..c08eb811532 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -28,7 +28,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end def starred - @projects = current_user.starred_projects.sorted_by_activity + @projects = current_user.viewable_starred_projects.sorted_by_activity @projects = filter_projects(@projects) @projects = @projects.includes(:namespace, :forked_from_project, :tags) @projects = @projects.sort(@sort = params[:sort]) diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 5abf97342c3..f9a1929c117 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -12,7 +12,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController respond_to do |format| format.html { redirect_to dashboard_todos_path, notice: todo_notice } - format.js { render nothing: true } + format.js { head :ok } format.json do render json: { count: @todos.size, done_count: current_user.todos.done.count } end @@ -24,7 +24,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController respond_to do |format| format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } - format.js { render nothing: true } + format.js { head :ok } format.json do find_todos render json: { count: @todos.size, done_count: current_user.todos.done.count } diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 1dce4a21729..4dda4e51f6a 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -25,7 +25,7 @@ class DashboardController < Dashboard::ApplicationController def load_events projects = if params[:filter] == "starred" - current_user.starred_projects + current_user.viewable_starred_projects else current_user.authorized_projects end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index d5ef33888c6..48dbf656e84 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -40,7 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController respond_to do |format| format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/health_check_controller.rb b/app/controllers/health_check_controller.rb new file mode 100644 index 00000000000..037da7d2bce --- /dev/null +++ b/app/controllers/health_check_controller.rb @@ -0,0 +1,22 @@ +class HealthCheckController < HealthCheck::HealthCheckController + before_action :validate_health_check_access! + + private + + def validate_health_check_access! + render_404 unless token_valid? + end + + def token_valid? + token = params[:token].presence || request.headers['TOKEN'] + token.present? && + ActiveSupport::SecurityUtils.variable_size_secure_compare( + token, + current_application_settings.health_check_access_token + ) + end + + def render_404 + render file: Rails.root.join('public', '404'), layout: false, status: '404' + end +end diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb new file mode 100644 index 00000000000..131a16dad9b --- /dev/null +++ b/app/controllers/jwt_controller.rb @@ -0,0 +1,49 @@ +class JwtController < ApplicationController + skip_before_action :authenticate_user! + skip_before_action :verify_authenticity_token + before_action :authenticate_project_or_user + + SERVICES = { + Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService, + } + + def auth + service = SERVICES[params[:service]] + return head :not_found unless service + + result = service.new(@project, @user, auth_params).execute + + render json: result, status: result[:http_status] + end + + private + + def authenticate_project_or_user + authenticate_with_http_basic do |login, password| + # if it's possible we first try to authenticate project with login and password + @project = authenticate_project(login, password) + return if @project + + @user = authenticate_user(login, password) + return if @user + + render_403 + end + end + + def auth_params + params.permit(:service, :scope, :account, :client_id) + end + + def authenticate_project(login, password) + if login == 'gitlab-ci-token' + Project.find_by(builds_enabled: true, runners_token: password) + end + end + + def authenticate_user(login, password) + user = Gitlab::Auth.find_in_gitlab_or_ldap(login, password) + Gitlab::Auth.rate_limit!(request.ip, success: user.present?, login: login) + user + end +end diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index c6bdd0602c1..0f54dfa4efc 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -32,7 +32,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController def verify_user_oauth_applications_enabled return if current_application_settings.user_oauth_applications? - redirect_to applications_profile_url + redirect_to profile_path end def set_index_vars diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index df98f56a1cd..f35d631df0c 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -97,7 +97,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController handle_signup_error end - def handle_service_ticket provider, ticket + def handle_service_ticket(provider, ticket) Gitlab::OAuth::Session.create provider, ticket session[:service_tickets] ||= {} session[:service_tickets][provider] = ticket diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb index 0ede9b8e21b..1c24c4db993 100644 --- a/app/controllers/profiles/emails_controller.rb +++ b/app/controllers/profiles/emails_controller.rb @@ -24,7 +24,7 @@ class Profiles::EmailsController < Profiles::ApplicationController respond_to do |format| format.html { redirect_to profile_emails_url } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index a12549d6bcb..830e0b9591b 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -32,7 +32,7 @@ class Profiles::KeysController < Profiles::ApplicationController respond_to do |format| format.html { redirect_to profile_keys_url } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 8f83fdd02bc..6a358fdcc05 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -1,7 +1,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController skip_before_action :check_2fa_requirement - def new + def show unless current_user.otp_secret current_user.otp_secret = User.generate_otp_secret(32) end @@ -12,21 +12,22 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController current_user.save! if current_user.changed? - if two_factor_authentication_required? + if two_factor_authentication_required? && !current_user.two_factor_enabled? if two_factor_grace_period_expired? - flash.now[:alert] = 'You must enable Two-factor Authentication for your account.' + flash.now[:alert] = 'You must enable Two-Factor Authentication for your account.' else grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours - flash.now[:alert] = "You must enable Two-factor Authentication for your account before #{l(grace_period_deadline)}." + flash.now[:alert] = "You must enable Two-Factor Authentication for your account before #{l(grace_period_deadline)}." end end @qr_code = build_qr_code + setup_u2f_registration end def create if current_user.validate_and_consume_otp!(params[:pin_code]) - current_user.two_factor_enabled = true + current_user.otp_required_for_login = true @codes = current_user.generate_otp_backup_codes! current_user.save! @@ -34,8 +35,23 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController else @error = 'Invalid pin code' @qr_code = build_qr_code + setup_u2f_registration + render 'show' + end + end + + # A U2F (universal 2nd factor) device's information is stored after successful + # registration, which is then used while 2FA authentication is taking place. + def create_u2f + @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges]) - render 'new' + if @u2f_registration.persisted? + session.delete(:challenges) + redirect_to profile_account_path, notice: "Your U2F device was registered!" + else + @qr_code = build_qr_code + setup_u2f_registration + render :show end end @@ -70,4 +86,21 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def issuer_host Gitlab.config.gitlab.host end + + # Setup in preparation of communication with a U2F (universal 2nd factor) device + # Actual communication is performed using a Javascript API + def setup_u2f_registration + @u2f_registration ||= U2fRegistration.new + @registration_key_handles = current_user.u2f_registrations.pluck(:key_handle) + u2f = U2F::U2F.new(u2f_app_id) + + registration_requests = u2f.registration_requests + sign_requests = u2f.authentication_requests(@registration_key_handles) + session[:challenges] = registration_requests.map(&:challenge) + + gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id, + register_requests: registration_requests, + sign_requests: sign_requests, + browser_supports_u2f: browser_supports_u2f? }) + end end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index be872a93fee..776ba92c9ab 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -26,7 +26,7 @@ class Projects::ApplicationController < ApplicationController project_path = "#{namespace}/#{id}" @project = Project.find_with_namespace(project_path) - if @project && can?(current_user, :read_project, @project) + if can?(current_user, :read_project, @project) && !@project.pending_delete? if @project.path_with_namespace != project_path redirect_to request.original_url.gsub(project_path, @project.path_with_namespace) end diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index cfea1266516..832d7deb57d 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -37,7 +37,7 @@ class Projects::ArtifactsController < Projects::ApplicationController private def build - @build ||= project.builds.unscoped.find_by!(id: params[:build_id]) + @build ||= project.builds.find_by!(id: params[:build_id]) end def artifacts_file diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index 72921b3aa14..5962f74c39b 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -10,10 +10,7 @@ class Projects::AvatarsController < Projects::ApplicationController return if cached_blob? - headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob)) - headers['Content-Disposition'] = 'inline' - headers['Content-Type'] = safe_content_type(@blob) - head :ok # 'render nothing: true' messes up the Content-Type + send_git_blob @repository, @blob else render_404 end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index d09e7375b67..dd9508da049 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -50,7 +50,7 @@ class Projects::BranchesController < Projects::ApplicationController redirect_to namespace_project_branches_path(@project.namespace, @project), status: 303 end - format.js { render status: status[:return_code] } + format.js { render nothing: true, status: status[:return_code] } end end diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index b8b9e78427d..14c82826342 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -26,9 +26,9 @@ class Projects::BuildsController < Projects::ApplicationController end def show - @builds = @project.ci_commits.find_by_sha(@build.sha).builds.order('id DESC') + @builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC') @builds = @builds.where("id not in (?)", @build.id) - @commit = @build.commit + @pipeline = @build.pipeline respond_to do |format| format.html @@ -38,6 +38,14 @@ class Projects::BuildsController < Projects::ApplicationController end end + def trace + respond_to do |format| + format.json do + render json: @build.trace_with_state(params[:state].presence).merge!(id: @build.id, status: @build.status) + end + end + end + def retry unless @build.retryable? return render_404 @@ -73,7 +81,7 @@ class Projects::BuildsController < Projects::ApplicationController private def build - @build ||= project.builds.unscoped.find_by!(id: params[:id]) + @build ||= project.builds.find_by!(id: params[:id]) end def build_path(build) diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index a202cb38692..20637fa46fe 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -17,12 +17,12 @@ class Projects::CommitController < Projects::ApplicationController def show apply_diff_view_cookie! - @line_notes = commit.notes.inline + @grouped_diff_notes = commit.notes.grouped_diff_notes + @note = @project.build_commit_note(commit) - @notes = commit.notes.not_inline.fresh + @notes = commit.notes.non_diff_notes.fresh @noteable = @commit - @comments_allowed = @reply_allowed = true - @comments_target = { + @comments_target = { noteable_type: 'Commit', commit_id: @commit.id } @@ -67,10 +67,10 @@ class Projects::CommitController < Projects::ApplicationController create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.", success_path: successful_change_path, failure_path: failed_change_path) end - + def cherry_pick assign_change_commit_vars(@commit.cherry_pick_branch_name) - + return render_404 if @target_branch.blank? create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.", @@ -99,12 +99,12 @@ class Projects::CommitController < Projects::ApplicationController @commit ||= @project.commit(params[:id]) end - def ci_commits - @ci_commits ||= project.ci_commits.where(sha: commit.sha) + def pipelines + @pipelines ||= project.pipelines.where(sha: commit.sha) end def ci_builds - @ci_builds ||= Ci::Build.where(commit: ci_commits) + @ci_builds ||= Ci::Build.where(pipeline: pipelines) end def define_show_vars @@ -117,8 +117,8 @@ class Projects::CommitController < Projects::ApplicationController @diff_refs = [commit.parent || commit, commit] @notes_count = commit.notes.count - @statuses = CommitStatus.where(commit: ci_commits) - @builds = Ci::Build.where(commit: ci_commits) + @statuses = CommitStatus.where(pipeline: pipelines) + @builds = Ci::Build.where(pipeline: pipelines) end def assign_change_commit_vars(mr_source_branch) diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 671d5c23024..af0b69a2442 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -22,7 +22,8 @@ class Projects::CompareController < Projects::ApplicationController @base_commit = @project.merge_base_commit(@base_ref, @head_ref) @diffs = compare.diffs(diff_options) @diff_refs = [@base_commit, @commit] - @line_notes = [] + @diff_notes_disabled = true + @grouped_diff_notes = {} end end diff --git a/app/controllers/projects/container_registry_controller.rb b/app/controllers/projects/container_registry_controller.rb new file mode 100644 index 00000000000..d1f46497207 --- /dev/null +++ b/app/controllers/projects/container_registry_controller.rb @@ -0,0 +1,34 @@ +class Projects::ContainerRegistryController < Projects::ApplicationController + before_action :verify_registry_enabled + before_action :authorize_read_container_image! + before_action :authorize_update_container_image!, only: [:destroy] + layout 'project' + + def index + @tags = container_registry_repository.tags + end + + def destroy + url = namespace_project_container_registry_index_path(project.namespace, project) + + if tag.delete + redirect_to url + else + redirect_to url, alert: 'Failed to remove tag' + end + end + + private + + def verify_registry_enabled + render_404 unless Gitlab.config.registry.enabled + end + + def container_registry_repository + @container_registry_repository ||= project.container_registry_repository + end + + def tag + @tag ||= container_registry_repository.tag(params[:id]) + end +end diff --git a/app/controllers/projects/find_file_controller.rb b/app/controllers/projects/find_file_controller.rb index 54a0c447aee..cf53ad0a670 100644 --- a/app/controllers/projects/find_file_controller.rb +++ b/app/controllers/projects/find_file_controller.rb @@ -1,26 +1,26 @@ -# Controller for viewing a repository's file structure
-class Projects::FindFileController < Projects::ApplicationController
- include ExtractsPath
- include ActionView::Helpers::SanitizeHelper
- include TreeHelper
-
- before_action :require_non_empty_project
- before_action :assign_ref_vars
- before_action :authorize_download_code!
-
- def show
- return render_404 unless @repository.commit(@ref)
-
- respond_to do |format|
- format.html
- end
- end
-
- def list
- file_paths = @repo.ls_files(@ref)
-
- respond_to do |format|
- format.json { render json: file_paths }
- end
- end
-end
+# Controller for viewing a repository's file structure +class Projects::FindFileController < Projects::ApplicationController + include ExtractsPath + include ActionView::Helpers::SanitizeHelper + include TreeHelper + + before_action :require_non_empty_project + before_action :assign_ref_vars + before_action :authorize_download_code! + + def show + return render_404 unless @repository.commit(@ref) + + respond_to do |format| + format.html + end + end + + def list + file_paths = @repo.ls_files(@ref) + + respond_to do |format| + format.json { render json: file_paths } + end + end +end diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb new file mode 100644 index 00000000000..348d6cf4d96 --- /dev/null +++ b/app/controllers/projects/git_http_controller.rb @@ -0,0 +1,147 @@ +class Projects::GitHttpController < Projects::ApplicationController + attr_reader :user + + # Git clients will not know what authenticity token to send along + skip_before_action :verify_authenticity_token + skip_before_action :repository + before_action :authenticate_user + before_action :ensure_project_found! + + # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) + # GET /foo/bar.git/info/refs?service=git-receive-pack (git push) + def info_refs + if upload_pack? && upload_pack_allowed? + render_ok + elsif receive_pack? && receive_pack_allowed? + render_ok + else + render_not_found + end + end + + # POST /foo/bar.git/git-upload-pack (git pull) + def git_upload_pack + if upload_pack? && upload_pack_allowed? + render_ok + else + render_not_found + end + end + + # POST /foo/bar.git/git-receive-pack" (git push) + def git_receive_pack + if receive_pack? && receive_pack_allowed? + render_ok + else + render_not_found + end + end + + private + + def authenticate_user + return if project && project.public? && upload_pack? + + authenticate_or_request_with_http_basic do |login, password| + auth_result = Gitlab::Auth.find(login, password, project: project, ip: request.ip) + + if auth_result.type == :ci && upload_pack? + @ci = true + elsif auth_result.type == :oauth && !upload_pack? + # Not allowed + else + @user = auth_result.user + end + + ci? || user + end + end + + def ensure_project_found! + render_not_found if project.blank? + end + + def project + return @project if defined?(@project) + + project_id, _ = project_id_with_suffix + if project_id.blank? + @project = nil + else + @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}") + end + end + + # This method returns two values so that we can parse + # params[:project_id] (untrusted input!) in exactly one place. + def project_id_with_suffix + id = params[:project_id] || '' + + %w[.wiki.git .git].each do |suffix| + if id.end_with?(suffix) + # Be careful to only remove the suffix from the end of 'id'. + # Accidentally removing it from the middle is how security + # vulnerabilities happen! + return [id.slice(0, id.length - suffix.length), suffix] + end + end + + # Something is wrong with params[:project_id]; do not pass it on. + [nil, nil] + end + + def upload_pack? + git_command == 'git-upload-pack' + end + + def receive_pack? + git_command == 'git-receive-pack' + end + + def git_command + if action_name == 'info_refs' + params[:service] + else + action_name.dasherize + end + end + + def render_ok + render json: Gitlab::Workhorse.git_http_ok(repository, user) + end + + def repository + _, suffix = project_id_with_suffix + if suffix == '.wiki.git' + project.wiki.repository + else + project.repository + end + end + + def render_not_found + render text: 'Not Found', status: :not_found + end + + def ci? + @ci.present? + end + + def upload_pack_allowed? + return false unless Gitlab.config.gitlab_shell.upload_pack + + if user + Gitlab::GitAccess.new(user, project).download_access_check.allowed? + else + ci? || project.public? + end + end + + def receive_pack_allowed? + return false unless Gitlab.config.gitlab_shell.receive_pack + + # Skip user authorization on upload request. + # It will be done by the pre-receive hook in the repository. + user.present? + end +end diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index dfa9bd259e8..a60027ff477 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -27,8 +27,10 @@ class Projects::HooksController < Projects::ApplicationController if !@project.empty_repo? status, message = TestHookService.new.execute(hook, current_user) - if status - flash[:notice] = 'Hook successfully executed.' + if status && status >= 200 && status < 400 + flash[:notice] = "Hook executed successfully: HTTP #{status}" + elsif status + flash[:alert] = "Hook executed successfully but returned HTTP #{status} #{message}" else flash[:alert] = "Hook execution failed: #{message}" end @@ -61,7 +63,8 @@ class Projects::HooksController < Projects::ApplicationController :push_events, :tag_push_events, :token, - :url + :url, + :wiki_page_events ) end end diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index 7756f0f0ed3..a1b84afcd91 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -20,6 +20,7 @@ class Projects::ImportsController < Projects::ApplicationController @project.import_retry else @project.import_start + @project.add_import_job end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 016f5dd0005..4e2d3bebb2e 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -1,6 +1,7 @@ class Projects::IssuesController < Projects::ApplicationController include ToggleSubscriptionAction include IssuableActions + include ToggleAwardEmoji before_action :module_enabled before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, @@ -62,7 +63,7 @@ class Projects::IssuesController < Projects::ApplicationController def show @note = @project.notes.new(noteable: @issue) - @notes = @issue.notes.nonawards.with_associations.fresh + @notes = @issue.notes.with_associations.fresh @noteable = @issue respond_to do |format| @@ -155,7 +156,12 @@ class Projects::IssuesController < Projects::ApplicationController def bulk_update result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute - redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" }) + + respond_to do |format| + format.json do + render json: { notice: "#{result[:count]} issues updated" } + end + end end protected @@ -169,6 +175,7 @@ class Projects::IssuesController < Projects::ApplicationController end alias_method :subscribable_resource, :issue alias_method :issuable, :issue + alias_method :awardable, :issue def authorize_read_issue! return render_404 unless can?(current_user, :read_issue, @issue) @@ -214,7 +221,10 @@ class Projects::IssuesController < Projects::ApplicationController :issues_ids, :assignee_id, :milestone_id, - :state_event + :state_event, + label_ids: [], + add_label_ids: [], + remove_label_ids: [] ) end end diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index ff771ea6d9c..0ca675623e5 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -5,13 +5,14 @@ class Projects::LabelsController < Projects::ApplicationController before_action :label, only: [:edit, :update, :destroy] before_action :authorize_read_label! before_action :authorize_admin_labels!, only: [ - :new, :create, :edit, :update, :generate, :destroy + :new, :create, :edit, :update, :generate, :destroy, :remove_priority, :set_priorities ] respond_to :js, :html def index - @labels = @project.labels.page(params[:page]) + @labels = @project.labels.unprioritized.page(params[:page]) + @prioritized_labels = @project.labels.prioritized respond_to do |format| format.html @@ -71,6 +72,30 @@ class Projects::LabelsController < Projects::ApplicationController end end + def remove_priority + respond_to do |format| + if label.update_attribute(:priority, nil) + format.json { render json: label } + else + message = label.errors.full_messages.uniq.join('. ') + format.json { render json: { message: message }, status: :unprocessable_entity } + end + end + end + + def set_priorities + Label.transaction do + params[:label_ids].each_with_index do |label_id, index| + label = @project.labels.find_by_id(label_id) + label.update_attribute(:priority, index) if label + end + end + + respond_to do |format| + format.json { render json: { message: 'success' } } + end + end + protected def module_enabled diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 9c147b3689e..67e7187c10d 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController include ToggleSubscriptionAction include DiffHelper include IssuableActions + include ToggleAwardEmoji before_action :module_enabled before_action :merge_request, only: [ @@ -57,9 +58,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.html - format.json { render json: @merge_request } - format.diff { render text: @merge_request.to_diff } - format.patch { render text: @merge_request.to_patch } + format.json { render json: @merge_request } + format.patch { render text: @merge_request.to_patch } + format.diff do + return render_404 unless @merge_request.diff_refs + + send_git_diff @project.repository, @merge_request.diff_refs + end end end @@ -73,12 +78,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController # but we need it for the "View file @ ..." link by deleted files @base_commit ||= @merge_request.first_commit.parent || @merge_request.first_commit - @comments_allowed = @reply_allowed = true @comments_target = { noteable_type: 'MergeRequest', noteable_id: @merge_request.id } - @line_notes = @merge_request.notes.where("line_code is not null") + + @grouped_diff_notes = @merge_request.notes.grouped_diff_notes respond_to do |format| format.html @@ -117,9 +122,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commit = @merge_request.last_commit @base_commit = @merge_request.diff_base_commit @diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare + @diff_notes_disabled = true - @ci_commit = @merge_request.ci_commit - @statuses = @ci_commit.statuses if @ci_commit + @pipeline = @merge_request.pipeline + @statuses = @pipeline.statuses if @pipeline @note_counts = Note.where(commit_id: @commits.map(&:id)). group(:commit_id).count @@ -189,13 +195,18 @@ class Projects::MergeRequestsController < Projects::ApplicationController return end + if params[:sha] != @merge_request.source_sha + @status = :sha_mismatch + return + end + TodoService.new.merge_merge_request(merge_request, current_user) @merge_request.update(merge_error: nil) - if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active? + if params[:merge_when_build_succeeds].present? && @merge_request.pipeline && @merge_request.pipeline.active? MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) - .execute(@merge_request) + .execute(@merge_request) @status = :merge_when_build_succeeds else MergeWorker.perform_async(@merge_request.id, current_user.id, params) @@ -204,7 +215,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def branch_from - #This is always source + # This is always source @source_project = @merge_request.nil? ? @project : @merge_request.source_project @commit = @repository.commit(params[:ref]) if params[:ref].present? render layout: false @@ -224,10 +235,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def ci_status - ci_commit = @merge_request.ci_commit - if ci_commit - status = ci_commit.status - coverage = ci_commit.try(:coverage) + pipeline = @merge_request.pipeline + if pipeline + status = pipeline.status + coverage = pipeline.try(:coverage) + + status ||= "preparing" else ci_service = @merge_request.source_project.ci_service status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) if ci_service @@ -237,8 +250,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end - status = "preparing" if status.nil? - response = { title: merge_request.title, sha: merge_request.last_commit_short_sha, @@ -264,6 +275,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end alias_method :subscribable_resource, :merge_request alias_method :issuable, :merge_request + alias_method :awardable, :merge_request def closes_issues @closes_issues ||= @merge_request.closes_issues @@ -299,8 +311,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController def define_show_vars # Build a note object for comment form @note = @project.notes.new(noteable: @merge_request) - @notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh - @discussions = Note.discussions_from_notes(@notes) + @notes = @merge_request.mr_and_commit_notes.inc_author.fresh + @discussions = @notes.discussions @noteable = @merge_request # Get commits from repository @@ -309,8 +321,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request_diff = @merge_request.merge_request_diff - @ci_commit = @merge_request.ci_commit - @statuses = @ci_commit.statuses if @ci_commit + @pipeline = @merge_request.pipeline + @statuses = @pipeline.statuses if @pipeline if @merge_request.locked_long_ago? @merge_request.unlock_mr @@ -319,8 +331,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def define_widget_vars - @ci_commit = @merge_request.ci_commit - @ci_commits = [@ci_commit].compact + @pipeline = @merge_request.pipeline + @pipelines = [@pipeline].compact closes_issues end @@ -333,7 +345,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController params.require(:merge_request).permit( :title, :assignee_id, :source_project_id, :source_branch, :target_project_id, :target_branch, :milestone_id, - :state_event, :description, :task_num, label_ids: [] + :state_event, :description, :task_num, :force_remove_source_branch, + label_ids: [] ) end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index f7b6d137bde..da2892bfb3f 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -75,7 +75,7 @@ class Projects::MilestonesController < Projects::ApplicationController respond_to do |format| format.html { redirect_to namespace_project_milestones_path } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 707a0d0e5c6..836f79ff080 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -1,9 +1,11 @@ class Projects::NotesController < Projects::ApplicationController + include ToggleAwardEmoji + # Authorize before_action :authorize_read_note! before_action :authorize_create_note!, only: [:create] before_action :authorize_admin_note!, only: [:update, :destroy] - before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle] + before_action :find_current_user_notes, only: [:index] def index current_fetched_at = Time.now.to_i @@ -43,7 +45,7 @@ class Projects::NotesController < Projects::ApplicationController end respond_to do |format| - format.js { render nothing: true } + format.js { head :ok } end end @@ -52,39 +54,16 @@ class Projects::NotesController < Projects::ApplicationController note.update_attribute(:attachment, nil) respond_to do |format| - format.js { render nothing: true } + format.js { head :ok } end end - def award_toggle - noteable = if note_params[:noteable_type] == "issue" - project.issues.find(note_params[:noteable_id]) - else - project.merge_requests.find(note_params[:noteable_id]) - end - - data = { - author: current_user, - is_award: true, - note: note_params[:note].delete(":") - } - - note = noteable.notes.find_by(data) - - if note - note.destroy - else - Notes::CreateService.new(project, current_user, note_params).execute - end - - render json: { ok: true } - end - private def note @note ||= @project.notes.find(params[:id]) end + alias_method :awardable, :note def note_to_html(note) render_to_string( @@ -96,7 +75,7 @@ class Projects::NotesController < Projects::ApplicationController end def note_to_discussion_html(note) - return unless note.for_diff_line? + return unless note.diff_note? if params[:view] == 'parallel' template = "projects/notes/_diff_notes_with_reply_parallel" @@ -120,7 +99,7 @@ class Projects::NotesController < Projects::ApplicationController end def note_to_discussion_with_diff_html(note) - return unless note.for_diff_line? + return unless note.diff_note? render_to_string( "projects/notes/_discussion", @@ -131,13 +110,20 @@ class Projects::NotesController < Projects::ApplicationController end def note_json(note) - if note.valid? + if note.is_a?(AwardEmoji) + { + valid: note.valid?, + award: true, + id: note.id, + name: note.name + } + elsif note.valid? { valid: true, id: note.id, discussion_id: note.discussion_id, html: note_to_html(note), - award: note.is_award, + award: false, note: note.note, discussion_html: note_to_discussion_html(note), discussion_with_diff_html: note_to_discussion_with_diff_html(note) @@ -145,7 +131,7 @@ class Projects::NotesController < Projects::ApplicationController else { valid: false, - award: note.is_award, + award: false, errors: note.errors } end @@ -158,7 +144,7 @@ class Projects::NotesController < Projects::ApplicationController def note_params params.require(:note).permit( :note, :noteable, :noteable_id, :noteable_type, :project_id, - :attachment, :line_code, :commit_id + :attachment, :line_code, :commit_id, :type ) end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb new file mode 100644 index 00000000000..cac440ae53e --- /dev/null +++ b/app/controllers/projects/pipelines_controller.rb @@ -0,0 +1,59 @@ +class Projects::PipelinesController < Projects::ApplicationController + before_action :pipeline, except: [:index, :new, :create] + before_action :commit, only: [:show] + before_action :authorize_read_pipeline! + before_action :authorize_create_pipeline!, only: [:new, :create] + before_action :authorize_update_pipeline!, only: [:retry, :cancel] + + def index + @scope = params[:scope] + all_pipelines = project.pipelines + @pipelines_count = all_pipelines.count + @running_or_pending_count = all_pipelines.running_or_pending.count + @pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope) + @pipelines = @pipelines.order(id: :desc).page(params[:page]).per(30) + end + + def new + @pipeline = project.pipelines.new(ref: @project.default_branch) + end + + def create + @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute + unless @pipeline.persisted? + render 'new' + return + end + + redirect_to namespace_project_pipeline_path(project.namespace, project, @pipeline) + end + + def show + end + + def retry + pipeline.retry_failed + + redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project) + end + + def cancel + pipeline.cancel_running + + redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project) + end + + private + + def create_params + params.require(:pipeline).permit(:ref) + end + + def pipeline + @pipeline ||= project.pipelines.find_by!(id: params[:id]) + end + + def commit + @commit ||= @pipeline.commit_data + end +end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 33b2625c0ac..cdea5f0b776 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -55,7 +55,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController format.html do redirect_to namespace_project_project_members_path(@project.namespace, @project) end - format.js { render nothing: true } + format.js { head :ok } end end @@ -81,7 +81,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController respond_to do |format| format.html { redirect_to dashboard_projects_path, notice: "You left the project." } - format.js { render nothing: true } + format.js { head :ok } end else if current_user == @project.owner diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index e49259c34b6..efa7bf14d0f 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -39,7 +39,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController respond_to do |format| format.html { redirect_to namespace_project_protected_branches_path } - format.js { render nothing: true } + format.js { head :ok } end end diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 10de0e60530..10d24da16d7 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -18,10 +18,7 @@ class Projects::RawController < Projects::ApplicationController if @blob.lfs_pointer? send_lfs_object else - headers.store(*Gitlab::Workhorse.send_git_blob(@repository, @blob)) - headers['Content-Disposition'] = 'inline' - headers['Content-Type'] = safe_content_type(@blob) - head :ok # 'render nothing: true' messes up the Content-Type + send_git_blob @repository, @blob end else render_404 diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index bb7a6b6a5ab..d5af0341d18 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -11,8 +11,7 @@ class Projects::RepositoriesController < Projects::ApplicationController end def archive - headers.store(*Gitlab::Workhorse.send_git_archive(@project, params[:ref], params[:format])) - head :ok + send_git_archive @repository, ref: params[:ref], format: params[:format] rescue => ex logger.error("#{self.class.name}: #{ex}") return git_not_found! diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index 0dd2d6a99be..0b4fa572501 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -20,7 +20,7 @@ class Projects::RunnersController < Projects::ApplicationController if @runner.update_attributes(runner_params) redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' else - redirect_to runner_path(@runner), alert: 'Runner was not updated.' + render 'edit' end end @@ -64,6 +64,6 @@ class Projects::RunnersController < Projects::ApplicationController end def runner_params - params.require(:runner).permit(:description, :tag_list, :active) + params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) end end diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index 00234654578..6f068729390 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -3,20 +3,44 @@ class Projects::VariablesController < Projects::ApplicationController layout 'project_settings' + def index + @variable = Ci::Variable.new + end + def show + @variable = @project.variables.find(params[:id]) end def update - if project.update_attributes(project_params) + @variable = @project.variables.find(params[:id]) + + if @variable.update_attributes(project_params) + redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variable was successfully updated.' + else + render action: "show" + end + end + + def create + @variable = Ci::Variable.new(project_params) + + if @variable.valid? && @project.variables << @variable redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.' else - render action: 'show' + render action: "index" end end + def destroy + @key = @project.variables.find(params[:id]) + @key.destroy + + redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variable was successfully removed.' + end + private def project_params - params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] }) + params.require(:variable).permit([:id, :key, :value, :_destroy]) end end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 0d6c32fabd2..2aa6bed0724 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -91,11 +91,11 @@ class Projects::WikisController < Projects::ApplicationController def markdown_preview text = params[:text] - ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user) - ext.analyze(text) + ext = Gitlab::ReferenceExtractor.new(@project, current_user) + ext.analyze(text, author: current_user) render json: { - body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki), + body: view_context.markdown(text, pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]), references: { users: ext.users.map(&:username) } diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3768efe142a..a6479c42d94 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -101,13 +101,7 @@ class ProjectsController < Projects::ApplicationController respond_to do |format| format.html do - if current_user - @membership = @project.team.find_member(current_user.id) - - if @membership - @notification_setting = current_user.notification_settings_for(@project) - end - end + @notification_setting = current_user.notification_settings_for(@project) if current_user if @project.repository_exists? if @project.empty_repo? @@ -145,8 +139,9 @@ class ProjectsController < Projects::ApplicationController participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id) @suggestions = { - emojis: AwardEmoji.urls, + emojis: Gitlab::AwardEmoji.urls, issues: autocomplete.issues, + milestones: autocomplete.milestones, mergerequests: autocomplete.merge_requests, members: participants } @@ -202,8 +197,8 @@ class ProjectsController < Projects::ApplicationController def markdown_preview text = params[:text] - ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user) - ext.analyze(text) + ext = Gitlab::ReferenceExtractor.new(@project, current_user) + ext.analyze(text, author: current_user) render json: { body: view_context.markdown(text), @@ -235,10 +230,11 @@ class ProjectsController < Projects::ApplicationController def project_params params.require(:project).permit( :name, :path, :description, :issues_tracker, :tag_list, :runners_token, - :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch, + :issues_enabled, :merge_requests_enabled, :snippets_enabled, :container_registry_enabled, + :issues_tracker_id, :default_branch, :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, - :public_builds, + :public_builds, :only_allow_merge_if_build_succeeds ) end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 352bff19383..75b78a49eab 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -37,8 +37,8 @@ class RegistrationsController < Devise::RegistrationsController super end - def after_sign_up_path_for(_resource) - users_almost_there_path + def after_sign_up_path_for(user) + user.confirmed? ? dashboard_projects_path : users_almost_there_path end def after_inactive_sign_up_path_for(_resource) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index c29f4609e93..dae8f7b1447 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,5 +1,6 @@ class SessionsController < Devise::SessionsController include AuthenticatesWithTwoFactor + include Devise::Controllers::Rememberable include Recaptcha::ClientHelper skip_before_action :check_2fa_requirement, only: [:destroy] @@ -13,6 +14,7 @@ class SessionsController < Devise::SessionsController before_action :load_recaptcha def new + set_minimum_password_length if Gitlab.config.ldap.enabled @ldap_servers = Gitlab::LDAP::Config.servers else @@ -29,8 +31,7 @@ class SessionsController < Devise::SessionsController resource.update_attributes(reset_password_token: nil, reset_password_sent_at: nil) end - authenticated_with = user_params[:otp_attempt] ? "two-factor" : "standard" - log_audit_event(current_user, with: authenticated_with) + log_audit_event(current_user, with: authentication_method) end end @@ -53,7 +54,7 @@ class SessionsController < Devise::SessionsController end def user_params - params.require(:user).permit(:login, :password, :remember_me, :otp_attempt) + params.require(:user).permit(:login, :password, :remember_me, :otp_attempt, :device_response) end def find_user @@ -88,26 +89,6 @@ class SessionsController < Devise::SessionsController find_user.try(:two_factor_enabled?) end - def authenticate_with_two_factor - user = self.resource = find_user - - if user_params[:otp_attempt].present? && session[:otp_user_id] - if valid_otp_attempt?(user) - # Remove any lingering user data from login - session.delete(:otp_user_id) - - sign_in(user) and return - else - flash.now[:alert] = 'Invalid two-factor code.' - render :two_factor and return - end - else - if user && user.valid_password?(user_params[:password]) - prompt_for_two_factor(user) - end - end - end - def auto_sign_in_with_provider provider = Gitlab.config.omniauth.auto_sign_in_with_provider return unless provider.present? @@ -136,4 +117,14 @@ class SessionsController < Devise::SessionsController def load_recaptcha Gitlab::Recaptcha.load_configurations! end + + def authentication_method + if user_params[:otp_attempt] + "two-factor" + elsif user_params[:device_response] + "two-factor-via-u2f-device" + else + "standard" + end + end end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 2daceed039b..2a17c1f34db 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -10,7 +10,7 @@ class SnippetsController < ApplicationController # Allow destroy snippet before_action :authorize_admin_snippet!, only: [:destroy] - skip_before_action :authenticate_user!, only: [:index, :user_index, :show, :raw] + skip_before_action :authenticate_user!, only: [:index, :show, :raw] layout 'snippets' respond_to :html diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2ae180c8a12..a99632454d9 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -58,11 +58,22 @@ class UsersController < ApplicationController end end + def snippets + load_snippets + + respond_to do |format| + format.html { render 'show' } + format.json do + render json: { + html: view_to_html_string("snippets/_snippets", collection: @snippets) + } + end + end + end + def calendar calendar = contributions_calendar @timestamps = calendar.timestamps - @starting_year = calendar.starting_year - @starting_month = calendar.starting_month render 'calendar', layout: false end @@ -116,6 +127,15 @@ class UsersController < ApplicationController @groups = JoinedGroupsFinder.new(user).execute(current_user) end + def load_snippets + @snippets = SnippetsFinder.new.execute( + current_user, + filter: :by_user, + user: user, + scope: params[:scope] + ).page(params[:page]) + end + def projects_for_current_user ProjectsFinder.new.execute(current_user) end diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb index 3b9a421b118..aa8f4c1d0e4 100644 --- a/app/finders/group_projects_finder.rb +++ b/app/finders/group_projects_finder.rb @@ -18,7 +18,7 @@ class GroupProjectsFinder < UnionFinder projects = [] if current_user - if @group.users.include?(current_user) + if @group.users.include?(current_user) || current_user.admin? projects << @group.projects unless only_shared projects << @group.shared_projects unless only_owned else diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index f00f3f709e9..a0932712bd0 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -224,7 +224,7 @@ class IssuableFinder def sort(items) # Ensure we always have an explicit sort order (instead of inheriting # multiple orders when combining ActiveRecord::Relation objects). - params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc) + params[:sort] ? items.sort(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc) end def by_assignee(items) @@ -250,12 +250,12 @@ class IssuableFinder def by_milestone(items) if milestones? if filter_by_no_milestone? - items = items.where(milestone_id: [-1, nil]) + items = items.left_joins_milestones.where(milestone_id: [-1, nil]) elsif filter_by_upcoming_milestone? - upcoming = Milestone.where(project_id: projects).upcoming - items = items.joins(:milestone).where(milestones: { title: upcoming.try(:title) }) + upcoming_ids = Milestone.upcoming_ids_by_projects(projects) + items = items.left_joins_milestones.where(milestone_id: upcoming_ids) else - items = items.joins(:milestone).where(milestones: { title: params[:milestone_title] }) + items = items.with_milestone(params[:milestone_title]) if projects items = items.where(milestones: { project_id: projects }) @@ -271,7 +271,7 @@ class IssuableFinder if filter_by_no_label? items = items.without_label else - items = items.with_label(label_names) + items = items.with_label(label_names, params[:sort]) if projects items = items.where(labels: { project_id: projects }) end @@ -318,7 +318,11 @@ class IssuableFinder end def label_names - params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name] + if labels? + params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name] + else + [] + end end def current_user_related? diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index fa4c635f55c..ee14ac60fb4 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -10,11 +10,11 @@ class NotesFinder notes = case target_type when "commit" - project.notes.for_commit_id(target_id).not_inline + project.notes.for_commit_id(target_id).non_diff_notes when "issue" - project.issues.find(target_id).notes.nonawards.inc_author + project.issues.find(target_id).notes.inc_author when "merge_request" - project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author + project.merge_requests.find(target_id).mr_and_commit_notes.inc_author when "snippet", "project_snippet" project.snippets.find(target_id).notes else diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb new file mode 100644 index 00000000000..c19a795d467 --- /dev/null +++ b/app/finders/pipelines_finder.rb @@ -0,0 +1,38 @@ +class PipelinesFinder + attr_reader :project + + def initialize(project) + @project = project + end + + def execute(pipelines, scope) + case scope + when 'running' + pipelines.running_or_pending + when 'branches' + from_ids(pipelines, ids_for_ref(pipelines, branches)) + when 'tags' + from_ids(pipelines, ids_for_ref(pipelines, tags)) + else + pipelines + end + end + + private + + def ids_for_ref(pipelines, refs) + pipelines.where(ref: refs).group(:ref).select('max(id)') + end + + def from_ids(pipelines, ids) + pipelines.unscoped.where(id: ids) + end + + def branches + project.repository.branches.map(&:name) + end + + def tags + project.repository.tags.map(&:name) + end +end diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 3ba27c40504..1d88116d7d2 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -30,13 +30,13 @@ class TodosFinder items = by_state(items) items = by_type(items) - items + items.reorder(id: :desc) end private def action_id? - action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED].include?(action_id.to_i) + action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED].include?(action_id.to_i) end def action_id @@ -78,6 +78,16 @@ class TodosFinder @project end + def projects + return @projects if defined?(@projects) + + if project? + @projects = project + else + @projects = ProjectsFinder.new.execute(current_user) + end + end + def type? type.present? && ['Issue', 'MergeRequest'].include?(type) end @@ -105,6 +115,8 @@ class TodosFinder def by_project(items) if project? items = items.where(project: project) + elsif projects + items = items.merge(projects).joins(:project) end items diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index e0abc3a2869..f240584ccbf 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -30,4 +30,8 @@ module AppearancesHelper render 'shared/logo.svg' end end + + def navbar_icon(icon_name) + render "shared/icons/#{icon_name}.svg" + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3e0074da394..439b015b3b8 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -110,8 +110,7 @@ module ApplicationHelper ] # If reference is commit id - we should add it to branch/tag selectbox - if(@ref && !options.flatten.include?(@ref) && - @ref =~ /\A[0-9a-zA-Z]{6,52}\z/) + if @ref && !options.flatten.include?(@ref) && @ref =~ /\A[0-9a-zA-Z]{6,52}\z/ options << ['Commit', [@ref]] end @@ -263,6 +262,8 @@ module ApplicationHelper assignee_id: params[:assignee_id], author_id: params[:author_id], sort: params[:sort], + issue_search: params[:issue_search], + label_name: params[:label_name] } options = exist_opts.merge(options) @@ -273,16 +274,11 @@ module ApplicationHelper end end - path = request.path - path << "?#{options.to_param}" - if add_label - if params[:label_name].present? and params[:label_name].respond_to?('any?') - params[:label_name].each do |label| - path << "&label_name[]=#{label}" - end - end - end - path + params = options.compact + + params.delete(:label_name) unless add_label + + "#{request.path}?#{params.to_param}" end def outdated_browser? diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 914b0ef6042..55313fd8357 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -15,6 +15,10 @@ module ApplicationSettingsHelper current_application_settings.sign_in_text end + def after_sign_up_text + current_application_settings.after_sign_up_text + end + def shared_runners_text current_application_settings.shared_runners_text end @@ -60,4 +64,18 @@ module ApplicationSettingsHelper end end end + + def oauth_providers_checkboxes + button_based_providers.map do |source| + disabled = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s) + css_class = 'btn' + css_class << ' active' unless disabled + checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]' + + label_tag(checkbox_name, class: css_class) do + check_box_tag(checkbox_name, source, !disabled, + autocomplete: 'off') + Gitlab::OAuth::Provider.label_for(source) + end + end + end end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index b4f80fd9b3e..cd4d778e508 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -38,6 +38,16 @@ module AuthHelper auth_providers.reject { |provider| form_based_provider?(provider) } end + def enabled_button_based_providers + disabled_providers = current_application_settings.disabled_oauth_sign_in_sources || [] + + button_based_providers.map(&:to_s) - disabled_providers + end + + def button_based_providers_enabled? + enabled_button_based_providers.any? + end + def provider_image_tag(provider, size = 64) label = label_for_provider(provider) @@ -56,7 +66,7 @@ module AuthHelper def two_factor_skippable? current_application_settings.require_two_factor_authentication && - !current_user.two_factor_enabled && + !current_user.two_factor_enabled? && current_application_settings.two_factor_grace_period && !two_factor_grace_period_expired? end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 93241b3afb7..cec2dc753fe 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -184,4 +184,14 @@ module BlobHelper Other: licenses.reject(&:featured).map { |license| [license.name, license.key] } } end + + def gitignore_names + return @gitignore_names if defined?(@gitignore_names) + + @gitignore_names = { + Global: Gitlab::Gitignore.global.map { |gitignore| { name: gitignore.name } }, + # Note that the key here doesn't cover it really + Languages: Gitlab::Gitignore.languages_frameworks.map{ |gitignore| { name: gitignore.name } } + } + end end diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index a9047ede8c5..f742922d926 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -30,7 +30,7 @@ module ButtonHelper content_tag :a, protocol, class: klass, - href: @project.http_url_to_repo, + href: project.http_url_to_repo, data: { html: true, placement: 'right', diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 417050b4132..07e5c146844 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -1,7 +1,7 @@ module CiStatusHelper - def ci_status_path(ci_commit) - project = ci_commit.project - builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha) + def ci_status_path(pipeline) + project = pipeline.project + builds_namespace_project_commit_path(project.namespace, project, pipeline.sha) end def ci_status_with_icon(status, target = nil) @@ -38,19 +38,30 @@ module CiStatusHelper icon(icon_name + ' fw') end - def render_ci_status(ci_commit, tooltip_placement: 'auto left') - # TODO: split this method into - # - render_commit_status - # - render_pipeline_status - link_to ci_icon_for_status(ci_commit.status), - ci_status_path(ci_commit), - class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}", - title: "Build #{ci_label_for_status(ci_commit.status)}", - data: { toggle: 'tooltip', placement: tooltip_placement } + def render_commit_status(commit, tooltip_placement: 'auto left') + project = commit.project + path = builds_namespace_project_commit_path(project.namespace, project, commit) + render_status_with_link('commit', commit.status, path, tooltip_placement) + end + + def render_pipeline_status(pipeline, tooltip_placement: 'auto left') + project = pipeline.project + path = namespace_project_pipeline_path(project.namespace, project, pipeline) + render_status_with_link('pipeline', pipeline.status, path, tooltip_placement) end def no_runners_for_project?(project) project.runners.blank? && Ci::Runner.shared.blank? end + + private + + def render_status_with_link(type, status, path, tooltip_placement) + link_to ci_icon_for_status(status), + path, + class: "ci-status-link ci-status-icon-#{status.dasherize}", + title: "#{type.titleize}: #{ci_label_for_status(status)}", + data: { toggle: 'tooltip', placement: tooltip_placement } + end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index b59c3982edd..d328f56c80c 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -123,13 +123,14 @@ module CommitsHelper ) end - def revert_commit_link(commit, continue_to_path, btn_class: nil) + def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) return unless current_user - tooltip = "Revert this #{commit.change_type_title} in a new merge request" + tooltip = "Revert this #{commit.change_type_title} in a new merge request" if has_tooltip if can_collaborate_with_project? - link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip" + btn_class = "btn btn-grouped btn-close btn-#{btn_class}" unless btn_class.nil? + link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" elsif can?(current_user, :fork_project, @project) continue_params = { to: continue_to_path, @@ -140,17 +141,20 @@ module CommitsHelper namespace_key: current_user.namespace.id, continue: continue_params) - link_to 'Revert', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip + btn_class = "btn btn-grouped btn-close" unless btn_class.nil? + + link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) end end - def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil) + def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) return unless current_user tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request" if can_collaborate_with_project? - link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip" + btn_class = "btn btn-default btn-grouped btn-#{btn_class}" unless btn_class.nil? + link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" elsif can?(current_user, :fork_project, @project) continue_params = { to: continue_to_path, @@ -161,7 +165,8 @@ module CommitsHelper namespace_key: current_user.namespace.id, continue: continue_params) - link_to 'Cherry-pick', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip + btn_class = "btn btn-grouped btn-close" unless btn_class.nil? + link_to 'Cherry-pick', fork_path, class: "#{btn_class}", method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) end end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 9f73edb4553..cbe47176831 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -2,8 +2,8 @@ module DiffHelper def mark_inline_diffs(old_line, new_line) old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_line, new_line).inline_diffs - marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs) - marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs) + marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs, mode: :deletion) + marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs, mode: :addition) [marked_old_line, marked_new_line] end @@ -39,11 +39,11 @@ module DiffHelper end def unfold_bottom_class(bottom) - (bottom) ? 'js-unfold-bottom' : '' + bottom ? 'js-unfold-bottom' : '' end def unfold_class(unfold) - (unfold) ? 'unfold js-unfold' : '' + unfold ? 'unfold js-unfold' : '' end def diff_line_content(line, line_type = nil) @@ -55,22 +55,18 @@ module DiffHelper end end - def line_comments - @line_comments ||= @line_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) - end - - def organize_comments(type_left, type_right, line_code_left, line_code_right) - comments_left = comments_right = nil + def organize_comments(left, right) + notes_left = notes_right = nil - unless type_left.nil? && type_right == 'new' - comments_left = line_comments[line_code_left] + unless left[:type].nil? && right[:type] == 'new' + notes_left = @grouped_diff_notes[left[:line_code]] end - unless type_left.nil? && type_right.nil? - comments_right = line_comments[line_code_right] + unless left[:type].nil? && right[:type].nil? + notes_right = @grouped_diff_notes[right[:line_code]] end - [comments_left, comments_right] + [notes_left, notes_right] end def inline_diff_btn @@ -96,8 +92,8 @@ module DiffHelper ].join(' ').html_safe end - def commit_for_diff(diff) - if diff.deleted_file + def commit_for_diff(diff_file) + if diff_file.deleted_file @base_commit || @commit.parent || @commit else @commit diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 14697f774cc..6b617e1730a 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -67,9 +67,9 @@ module DropdownsHelper end end - def dropdown_filter(placeholder) + def dropdown_filter(placeholder, search_id: nil) content_tag :div, class: "dropdown-input" do - filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder + filter_output = search_field_tag search_id, nil, class: "dropdown-input-field", placeholder: placeholder filter_output << icon('search', class: "dropdown-input-search") filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button") diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 41b5bd7be90..8466d0aa0ba 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -32,12 +32,6 @@ module EmailsHelper nil end - def color_email_diff(diffcontent) - formatter = Rouge::Formatters::HTML.new(css_class: 'highlight', inline_theme: 'github') - lexer = Rouge::Lexers::Diff - raw formatter.format(lexer.lex(diffcontent)) - end - def password_reset_token_valid_time valid_hours = Devise.reset_password_within / 60 / 60 if valid_hours >= 24 diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 592bad8ba24..bfedcb1c42b 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -3,7 +3,7 @@ module EventsHelper author = event.author if author - link_to author.name, user_path(author.username), title: h(author.name) + link_to author.name, user_path(author.username), title: author.name else event.author_name end @@ -39,15 +39,6 @@ module EventsHelper end end - def icon_for_event - { - EventFilter.push => 'upload', - EventFilter.merged => 'check-square-o', - EventFilter.comments => 'comments', - EventFilter.team => 'user', - } - end - def event_preposition(event) if event.push? || event.commented? || event.target "at" @@ -66,11 +57,7 @@ module EventsHelper words << event.ref_name words << "at" elsif event.commented? - if event.note_commit? - words << event.note_short_commit_id - else - words << "##{truncate event.note_target_iid}" - end + words << event.note_target_reference words << "at" elsif event.milestone? words << "##{event.target_iid}" if event.target_iid @@ -93,21 +80,12 @@ module EventsHelper elsif event.merge_request? namespace_project_merge_request_url(event.project.namespace, event.project, event.merge_request) - elsif event.note? && event.note_commit? + elsif event.note? && event.commit_note? namespace_project_commit_url(event.project.namespace, event.project, event.note_target) elsif event.note? if event.note_target - if event.note_commit? - namespace_project_commit_path(event.project.namespace, event.project, - event.note_commit_id, - anchor: dom_id(event.target)) - elsif event.note_project_snippet? - namespace_project_snippet_path(event.project.namespace, - event.project, event.note_target) - else - event_note_target_path(event) - end + event_note_target_path(event) end elsif event.push? push_event_feed_url(event) @@ -143,42 +121,30 @@ module EventsHelper end def event_note_target_path(event) - if event.note? && event.note_commit? - namespace_project_commit_path(event.project.namespace, event.project, - event.note_target) + if event.note? && event.commit_note? + namespace_project_commit_path(event.project.namespace, + event.project, + event.note_target, + anchor: dom_id(event.target)) + elsif event.project_snippet_note? + namespace_project_snippet_path(event.project.namespace, + event.project, + event.note_target, + anchor: dom_id(event.target)) else polymorphic_path([event.project.namespace.becomes(Namespace), event.project, event.note_target], - anchor: dom_id(event.target)) + anchor: dom_id(event.target)) end end def event_note_title_html(event) if event.note_target - if event.note_commit? - link_to( - namespace_project_commit_path(event.project.namespace, event.project, - event.note_commit_id, - anchor: dom_id(event.target), title: h(event.target_title)), - class: "commit_short_id" - ) do - "#{event.note_target_type} #{event.note_short_commit_id}" - end - elsif event.note_project_snippet? - link_to(namespace_project_snippet_path(event.project.namespace, - event.project, - event.note_target), title: h(event.project.name)) do - "#{event.note_target_type} #{truncate event.note_target.to_reference}" - end - else - link_to event_note_target_path(event) do - "#{event.note_target_type} #{truncate event.note_target.to_reference}" - end + link_to(event_note_target_path(event), title: event.target_title, class: 'has-tooltip') do + "#{event.note_target_type} #{event.note_target_reference}" end else - content_tag :strong do - "(deleted)" - end + content_tag(:strong, '(deleted)') end end @@ -193,28 +159,6 @@ module EventsHelper "--broken encoding" end - def event_to_atom(xml, event) - if event.visible_to_user?(current_user) - xml.entry do - event_link = event_feed_url(event) - event_title = event_feed_title(event) - event_summary = event_feed_summary(event) - - xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}" - xml.link href: event_link - xml.title truncate(event_title, length: 80) - xml.updated event.created_at.xmlschema - xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email)) - xml.author do |author| - xml.name event.author_name - xml.email event.author_email - end - - xml.summary(type: "xhtml") { |x| x << event_summary unless event_summary.nil? } - end - end - end - def event_row_class(event) if event.body? "event-block" diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 3a45205563e..067a00660aa 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -13,7 +13,7 @@ module GitlabMarkdownHelper def link_to_gfm(body, url, html_options = {}) return "" if body.blank? - escaped_body = if body =~ /\A\<img/ + escaped_body = if body.start_with?('<img') body else escape_once(body) @@ -108,7 +108,7 @@ module GitlabMarkdownHelper def render_wiki_content(wiki_page) case wiki_page.format when :markdown - markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki) + markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki, page_slug: wiki_page.slug) when :asciidoc asciidoc(wiki_page.content) else diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index f07eff3fb57..2ce2d4e694f 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -33,6 +33,10 @@ module GitlabRoutingHelper namespace_project_builds_path(project.namespace, project, *args) end + def project_container_registry_path(project, *args) + namespace_project_container_registry_index_path(project.namespace, project, *args) + end + def activity_project_path(project, *args) activity_namespace_project_path(project.namespace, project, *args) end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index b1f0a765bb9..4cac69c6795 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -31,7 +31,7 @@ module GroupsHelper if group && group.avatar.present? group.avatar.url else - 'no_group_avatar.png' + image_path('no_group_avatar.png') end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 39474217286..40d8ce8a1d3 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -8,14 +8,6 @@ module IssuablesHelper "right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}" end - def issuables_count(issuable) - base_issuable_scope(issuable).maximum(:iid) - end - - def next_issuable_for(issuable) - base_issuable_scope(issuable).where('iid > ?', issuable.iid).last - end - def multi_label_name(current_labels, default_label) # current_labels may be a string from before if current_labels.is_a?(Array) @@ -45,19 +37,11 @@ module IssuablesHelper end end - def prev_issuable_for(issuable) - base_issuable_scope(issuable).where('iid < ?', issuable.iid).first - end - def user_dropdown_label(user_id, default_label) + return default_label if user_id.nil? return "Unassigned" if user_id == "0" - if @project - member = @project.team.find_member(user_id) - user = member.user if member - else - user = User.find_by(id: user_id) - end + user = User.find_by(id: user_id) if user user.name @@ -76,7 +60,7 @@ module IssuablesHelper def issuable_meta(issuable, project, text) output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier" - output << " opened #{time_ago_with_tooltip(issuable.created_at)} by".html_safe + output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe output << content_tag(:strong) do author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs") author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg") @@ -100,5 +84,4 @@ module IssuablesHelper issuable.open? ? :opened : :closed end end - end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 198d39455d7..72bd1fbbd81 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -105,23 +105,6 @@ module IssuesHelper return 'hidden' if issue.closed? == closed end - def issue_to_atom(xml, issue) - xml.entry do - xml.id namespace_project_issue_url(issue.project.namespace, - issue.project, issue) - xml.link href: namespace_project_issue_url(issue.project.namespace, - issue.project, issue) - xml.title truncate(issue.title, length: 80) - xml.updated issue.created_at.xmlschema - xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email)) - xml.author do |author| - xml.name issue.author_name - xml.email issue.author_email - end - xml.summary issue.title - end - end - def merge_requests_sentence(merge_requests) # Sorting based on the `!123` or `group/project!123` reference will sort # local merge requests first. @@ -162,16 +145,14 @@ module IssuesHelper end end - def emoji_author_list(notes, current_user) - list = notes.map do |note| - note.author == current_user ? "me" : note.author.name - end - - list.join(", ") + def award_user_list(awards, current_user) + awards.map do |award| + award.user == current_user ? 'me' : award.user.name + end.join(', ') end - def note_active_class(notes, current_user) - if current_user && notes.pluck(:author_id).include?(current_user.id) + def award_active_class(awards, current_user) + if current_user && awards.find { |a| a.user_id == current_user.id } "active" else "" diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb new file mode 100644 index 00000000000..91dd91718dc --- /dev/null +++ b/app/helpers/javascript_helper.rb @@ -0,0 +1,7 @@ +module JavascriptHelper + def page_specific_javascripts(js = nil) + @page_specific_javascripts = js unless js.nil? + + @page_specific_javascripts + end +end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index c99b137cdaa..5074e645769 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -32,7 +32,7 @@ module LabelsHelper # link_to_label(label) { "My Custom Label Text" } # # Returns a String - def link_to_label(label, project: nil, type: :issue, tooltip: true, &block) + def link_to_label(label, project: nil, type: :issue, tooltip: true, css_class: nil, &block) project ||= @project || label.project link = send("namespace_project_#{type.to_s.pluralize}_path", project.namespace, @@ -40,9 +40,9 @@ module LabelsHelper label_name: [label.name]) if block_given? - link_to link, &block + link_to link, class: css_class, &block else - link_to render_colored_label(label, tooltip: tooltip), link + link_to render_colored_label(label, tooltip: tooltip), link, class: css_class end end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 87fc2db6901..b3e6e468ecd 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -56,7 +56,7 @@ module MilestonesHelper def milestone_remaining_days(milestone) if milestone.expired? - content_tag(:strong, 'expired') + content_tag(:strong, 'Past due') elsif milestone.due_date days = milestone.remaining_days content = content_tag(:strong, days) diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 3aa41030453..469accf3142 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -30,17 +30,21 @@ module NavHelper else "page-gutter right-sidebar-expanded" end + elsif current_path?('builds#show') + "page-gutter build-sidebar right-sidebar-expanded" end end def nav_header_class - class_name = - if nav_menu_collapsed? - "header-collapsed" - else - "header-expanded" - end - class_name += " with-horizontal-nav" if defined?(nav) && nav + class_name = " with-horizontal-nav" if defined?(nav) && nav class_name end + + def layout_nav_class + "page-with-layout-nav" if defined?(nav) && nav + end + + def nav_control_class + "nav-control" if current_user + end end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 95072b5373f..b401c8385be 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -1,7 +1,7 @@ module NotesHelper # Helps to distinguish e.g. commit notes in mr notes list def note_for_main_target?(note) - (@noteable.class.name == note.noteable_type && !note.for_diff_line?) + @noteable.class.name == note.noteable_type && !note.diff_note? end def note_target_fields(note) @@ -15,16 +15,6 @@ module NotesHelper note.editable? && can?(current_user, :admin_note, note) end - def link_to_commit_diff_line_note(note) - if note.for_commit_diff_line? - link_to( - "#{note.diff_file_name}:L#{note.diff_new_line}", - namespace_project_commit_path(@project.namespace, @project, - note.noteable, anchor: note.line_code) - ) - end - end - def noteable_json(noteable) { id: noteable.id, @@ -35,7 +25,7 @@ module NotesHelper end def link_to_new_diff_note(line_code, line_type = nil) - discussion_id = Note.build_discussion_id( + discussion_id = LegacyDiffNote.build_discussion_id( @comments_target[:noteable_type], @comments_target[:noteable_id] || @comments_target[:commit_id], line_code @@ -45,9 +35,10 @@ module NotesHelper noteable_type: @comments_target[:noteable_type], noteable_id: @comments_target[:noteable_id], commit_id: @comments_target[:commit_id], + line_type: line_type, line_code: line_code, - discussion_id: discussion_id, - line_type: line_type + note_type: LegacyDiffNote.name, + discussion_id: discussion_id } button_tag(class: 'btn add-diff-note js-add-diff-note-button', @@ -57,18 +48,24 @@ module NotesHelper end end - def link_to_reply_diff(note, line_type = nil) + def link_to_reply_discussion(note, line_type = nil) return unless current_user data = { noteable_type: note.noteable_type, noteable_id: note.noteable_id, commit_id: note.commit_id, - line_code: note.line_code, discussion_id: note.discussion_id, line_type: line_type } + if note.diff_note? + data.merge!( + line_code: note.line_code, + note_type: LegacyDiffNote.name + ) + end + button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', data: data, title: 'Add a reply' end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 54ab9179efc..b8e64b3890a 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -31,6 +31,21 @@ module NotificationsHelper end end + def notification_description(level) + case level.to_sym + when :participating + 'You will only receive notifications from related resources' + when :mention + 'You will receive notifications only for comments in which you were @mentioned' + when :watch + 'You will receive notifications for any activity' + when :disabled + 'You will not get any notifications via email' + when :global + 'Use your global notification setting' + end + end + def notification_list_item(level, setting) title = notification_title(level) @@ -39,9 +54,10 @@ module NotificationsHelper notification_title: title } - content_tag(:li, class: ('active' if setting.level == level)) do - link_to '#', class: 'update-notification', data: data do - notification_icon(level, title) + content_tag(:li, role: "menuitem") do + link_to '#', class: "update-notification #{('is-active' if setting.level == level)}", data: data do + link_output = content_tag(:strong, title, class: 'dropdown-menu-inner-title') + link_output << content_tag(:span, notification_description(level), class: 'dropdown-menu-inner-content') end end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index f17d02a7a3f..5e5d170a9f3 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -124,11 +124,7 @@ module ProjectsHelper end def license_short_name(project) - no_license_key = project.repository.license_key.nil? || - # Back-compat if cache contains 'no-license', can be removed in a few weeks - project.repository.license_key == 'no-license' - - return 'LICENSE' if no_license_key + return 'LICENSE' if project.repository.license_key.nil? license = Licensee::License.new(project.repository.license_key) @@ -138,20 +134,28 @@ module ProjectsHelper private def get_project_nav_tabs(project, current_user) - nav_tabs = [:home, :forks] + nav_tabs = [:home] if !project.empty_repo? && can?(current_user, :download_code, project) - nav_tabs << [:files, :commits, :network, :graphs] + nav_tabs << [:files, :commits, :network, :graphs, :forks] end if project.repo_exists? && can?(current_user, :read_merge_request, project) nav_tabs << :merge_requests end + if can?(current_user, :read_pipeline, project) + nav_tabs << :pipelines + end + if can?(current_user, :read_build, project) nav_tabs << :builds end + if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project) + nav_tabs << :container_registry + end + if can?(current_user, :admin_project, project) nav_tabs << :settings end @@ -205,7 +209,8 @@ module ProjectsHelper end def default_url_to_repo(project = @project) - if default_clone_protocol == "ssh" + case default_clone_protocol + when 'ssh' project.ssh_url_to_repo else project.http_url_to_repo diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 24c4c098c65..d2f94d4ae6f 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -59,7 +59,7 @@ module SearchHelper # Autocomplete results for the current project, if it's defined def project_autocomplete if @project && @project.repository.exists? && @project.repository.root_ref - ref = @ref || @project.repository.root_ref + ref = @ref || @project.repository.root_ref [ { category: "Current Project", label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index e951a87a212..bb395e37884 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -18,7 +18,7 @@ module SelectsHelper first_user: first_user, current_user: opts[:current_user] || false, "push-code-to-protected-branches" => opts[:push_code_to_protected_branches], - author_id: opts[:author_id] || '' + author_id: opts[:author_id] || '' } } diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 630e10ea892..d86f1999f5c 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -14,7 +14,8 @@ module SortingHelper sort_value_recently_signin => sort_title_recently_signin, sort_value_oldest_signin => sort_title_oldest_signin, sort_value_downvotes => sort_title_downvotes, - sort_value_upvotes => sort_title_upvotes + sort_value_upvotes => sort_title_upvotes, + sort_value_priority => sort_title_priority } end @@ -28,6 +29,10 @@ module SortingHelper } end + def sort_title_priority + 'Priority' + end + def sort_title_oldest_updated 'Oldest updated' end @@ -84,6 +89,10 @@ module SortingHelper 'Most popular' end + def sort_value_priority + 'priority' + end + def sort_value_oldest_updated 'updated_asc' end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 96a83671009..563ddd2a511 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -95,7 +95,9 @@ module TabHelper end def project_tab_class - return "active" if current_page?(controller: "/projects", action: :edit, id: @project) + if controller.controller_path.start_with?('projects') + return 'active' + end if ['services', 'hooks', 'deploy_keys', 'protected_branches'].include? controller.controller_name "active" @@ -112,7 +114,7 @@ module TabHelper end def profile_tab_class - if controller.controller_path =~ /\Aprofiles/ + if controller.controller_path.start_with?('profiles') return 'active' end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 2f066682180..b4923fbb138 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -11,12 +11,15 @@ module TodosHelper case todo.action when Todo::ASSIGNED then 'assigned you' when Todo::MENTIONED then 'mentioned you on' + when Todo::BUILD_FAILED then 'The build failed for your' end end def todo_target_link(todo) target = todo.target_type.titleize.downcase - link_to "#{target} #{todo.target_reference}", todo_target_path(todo), { title: todo.target.title } + link_to "#{target} #{todo.target_reference}", todo_target_path(todo), + class: 'has-tooltip', + title: todo.target.title end def todo_target_path(todo) @@ -28,8 +31,21 @@ module TodosHelper namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project, todo.target, anchor: anchor) else - polymorphic_path([todo.project.namespace.becomes(Namespace), - todo.project, todo.target], anchor: anchor) + path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target] + + path.unshift(:builds) if todo.build_failed? + + polymorphic_path(path, anchor: anchor) + end + end + + def todo_target_state_pill(todo) + return unless show_todo_state?(todo) + + content_tag(:span, nil, class: 'target-status') do + content_tag(:span, nil, class: "status-box status-box-#{todo.target.state.dasherize}") do + todo.target.state.capitalize + end end end @@ -91,4 +107,10 @@ module TodosHelper options_from_collection_for_select(types, 'name', 'title', params[:type]) end + + private + + def show_todo_state?(todo) + (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && ['closed', 'merged'].include?(todo.target.state) + end end diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb new file mode 100644 index 00000000000..2bd0dbfd095 --- /dev/null +++ b/app/helpers/workhorse_helper.rb @@ -0,0 +1,24 @@ +# Helpers to send Git blobs, diffs or archives through Workhorse. +# Workhorse will also serve files when using `send_file`. +module WorkhorseHelper + # Send a Git blob through Workhorse + def send_git_blob(repository, blob) + headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob)) + headers['Content-Disposition'] = 'inline' + headers['Content-Type'] = safe_content_type(blob) + head :ok # 'render nothing: true' messes up the Content-Type + end + + # Send a Git diff through Workhorse + def send_git_diff(repository, diff_refs) + headers.store(*Gitlab::Workhorse.send_git_diff(repository, diff_refs)) + headers['Content-Disposition'] = 'inline' + head :ok + end + + # Archive a Git repository and send it through Workhorse + def send_git_archive(repository, ref:, format:) + headers.store(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format)) + head :ok + end +end diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb index b616add283a..415f6e12885 100644 --- a/app/mailers/devise_mailer.rb +++ b/app/mailers/devise_mailer.rb @@ -1,4 +1,6 @@ class DeviseMailer < Devise::Mailer default from: "#{Gitlab.config.gitlab.email_display_name} <#{Gitlab.config.gitlab.email_from}>" default reply_to: Gitlab.config.gitlab.email_reply_to + + layout 'devise_mailer' end diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 377c2999d6c..fdf1e9f5afc 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -59,20 +59,20 @@ module Emails subject: subject("Project was moved")) end - def repository_push_email(project_id, recipient, opts = {}) + def repository_push_email(project_id, opts = {}) @message = - Gitlab::Email::Message::RepositoryPush.new(self, project_id, recipient, opts) + Gitlab::Email::Message::RepositoryPush.new(self, project_id, opts) # used in notify layout @target_url = @message.target_url - @project = Project.find project_id + @project = Project.find(project_id) + @diff_notes_disabled = true add_project_headers headers['X-GitLab-Author'] = @message.author_username mail(from: sender(@message.author_id, @message.send_from_committer_email?), reply_to: @message.reply_to, - to: @message.recipient, subject: @message.subject) end end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 826e5f96fa1..1c663bdd521 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -10,6 +10,8 @@ class Notify < BaseMailer include Emails::Builds add_template_helper MergeRequestsHelper + add_template_helper DiffHelper + add_template_helper BlobHelper add_template_helper EmailsHelper def test_email(recipient_email, subject, body) diff --git a/app/models/ability.rb b/app/models/ability.rb index 6103a2947e2..44515550d9e 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -23,20 +23,41 @@ class Ability end.concat(global_abilities(user)) end + # Given a list of users and a project this method returns the users that can + # read the given project. + def users_that_can_read_project(users, project) + if project.public? + users + else + users.select do |user| + if user.admin? + true + elsif project.internal? && !user.external? + true + elsif project.owner == user + true + elsif project.team.members.include?(user) + true + else + false + end + end + end + end + # List of possible abilities for anonymous user def anonymous_abilities(user, subject) - case true - when subject.is_a?(PersonalSnippet) + if subject.is_a?(PersonalSnippet) anonymous_personal_snippet_abilities(subject) - when subject.is_a?(ProjectSnippet) + elsif subject.is_a?(ProjectSnippet) anonymous_project_snippet_abilities(subject) - when subject.is_a?(CommitStatus) + elsif subject.is_a?(CommitStatus) anonymous_commit_status_abilities(subject) - when subject.is_a?(Project) || subject.respond_to?(:project) + elsif subject.is_a?(Project) || subject.respond_to?(:project) anonymous_project_abilities(subject) - when subject.is_a?(Group) || subject.respond_to?(:group) + elsif subject.is_a?(Group) || subject.respond_to?(:group) anonymous_group_abilities(subject) - when subject.is_a?(User) + elsif subject.is_a?(User) anonymous_user_abilities else [] @@ -60,7 +81,9 @@ class Ability :read_project_member, :read_merge_request, :read_note, + :read_pipeline, :read_commit_status, + :read_container_image, :download_code ] @@ -203,6 +226,8 @@ class Ability :admin_label, :read_commit_status, :read_build, + :read_container_image, + :read_pipeline, ] end @@ -214,9 +239,13 @@ class Ability :update_commit_status, :create_build, :update_build, + :create_pipeline, + :update_pipeline, :create_merge_request, :create_wiki, - :push_code + :push_code, + :create_container_image, + :update_container_image, ] end @@ -242,7 +271,9 @@ class Ability :admin_wiki, :admin_project, :admin_commit_status, - :admin_build + :admin_build, + :admin_container_image, + :admin_pipeline ] end @@ -285,6 +316,11 @@ class Ability unless project.builds_enabled rules += named_abilities('build') + rules += named_abilities('pipeline') + end + + unless project.container_registry_enabled + rules += named_abilities('container_image') end rules diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index b61f5123127..b01a244032d 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: abuse_reports -# -# id :integer not null, primary key -# reporter_id :integer -# user_id :integer -# message :text -# created_at :datetime -# updated_at :datetime -# - class AbuseReport < ActiveRecord::Base belongs_to :reporter, class_name: 'User' belongs_to :user diff --git a/app/models/appearance.rb b/app/models/appearance.rb index 4528760fefa..4cf8dd9a8ce 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -1,16 +1,3 @@ -# == Schema Information -# -# Table name: appearances -# -# id :integer not null, primary key -# title :string -# description :text -# header_logo :string -# logo :string -# created_at :datetime not null -# updated_at :datetime not null -# - class Appearance < ActiveRecord::Base validates :title, presence: true validates :description, presence: true diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 72ec91d2909..a744f937918 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -1,63 +1,13 @@ -# == Schema Information -# -# Table name: application_settings -# -# id :integer not null, primary key -# default_projects_limit :integer -# signup_enabled :boolean -# signin_enabled :boolean -# gravatar_enabled :boolean -# sign_in_text :text -# created_at :datetime -# updated_at :datetime -# home_page_url :string -# default_branch_protection :integer default(2) -# restricted_visibility_levels :text -# version_check_enabled :boolean default(TRUE) -# max_attachment_size :integer default(10), not null -# default_project_visibility :integer -# default_snippet_visibility :integer -# restricted_signup_domains :text -# user_oauth_applications :boolean default(TRUE) -# after_sign_out_path :string -# session_expire_delay :integer default(10080), not null -# import_sources :text -# help_page_text :text -# admin_notification_email :string -# shared_runners_enabled :boolean default(TRUE), not null -# max_artifacts_size :integer default(100), not null -# runners_registration_token :string -# require_two_factor_authentication :boolean default(FALSE) -# two_factor_grace_period :integer default(48) -# metrics_enabled :boolean default(FALSE) -# metrics_host :string default("localhost") -# metrics_pool_size :integer default(16) -# metrics_timeout :integer default(10) -# metrics_method_call_threshold :integer default(10) -# recaptcha_enabled :boolean default(FALSE) -# recaptcha_site_key :string -# recaptcha_private_key :string -# metrics_port :integer default(8089) -# metrics_sample_interval :integer default(15) -# sentry_enabled :boolean default(FALSE) -# sentry_dsn :string -# akismet_enabled :boolean default(FALSE) -# akismet_api_key :string -# email_author_in_body :boolean default(FALSE) -# default_group_visibility :integer -# repository_checks_enabled :boolean default(FALSE) -# metrics_packet_size :integer default(1) -# shared_runners_text :text -# - class ApplicationSetting < ActiveRecord::Base include TokenAuthenticatable add_authentication_token_field :runners_registration_token + add_authentication_token_field :health_check_access_token CACHE_KEY = 'application_setting.last' serialize :restricted_visibility_levels serialize :import_sources + serialize :disabled_oauth_sign_in_sources, Array serialize :restricted_signup_domains, Array attr_accessor :restricted_signup_domains_raw @@ -101,6 +51,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than: 0 } + validates :container_registry_token_expire_delay, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + validates_each :restricted_visibility_levels do |record, attr, value| unless value.nil? value.each do |level| @@ -121,7 +75,18 @@ class ApplicationSetting < ActiveRecord::Base end end + validates_each :disabled_oauth_sign_in_sources do |record, attr, value| + unless value.nil? + value.each do |source| + unless Devise.omniauth_providers.include?(source.to_sym) + record.errors.add(attr, "'#{source}' is not an OAuth sign-in source") + end + end + end + end + before_save :ensure_runners_registration_token + before_save :ensure_health_check_access_token after_commit do Rails.cache.write(CACHE_KEY, self) @@ -137,6 +102,10 @@ class ApplicationSetting < ActiveRecord::Base Rails.cache.delete(CACHE_KEY) end + def self.cached + Rails.cache.fetch(CACHE_KEY) + end + def self.create_from_defaults create( default_projects_limit: Settings.gitlab['default_projects_limit'], @@ -144,7 +113,10 @@ class ApplicationSetting < ActiveRecord::Base signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], gravatar_enabled: Settings.gravatar['enabled'], - sign_in_text: Settings.extra['sign_in_text'], + sign_in_text: nil, + after_sign_up_text: nil, + help_page_text: nil, + shared_runners_text: nil, restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], max_attachment_size: Settings.gitlab['max_attachment_size'], session_expire_delay: Settings.gitlab['session_expire_delay'], @@ -159,6 +131,9 @@ class ApplicationSetting < ActiveRecord::Base recaptcha_enabled: false, akismet_enabled: false, repository_checks_enabled: true, + disabled_oauth_sign_in_sources: [], + send_user_confirmation_email: false, + container_registry_token_expire_delay: 5, ) end @@ -185,4 +160,8 @@ class ApplicationSetting < ActiveRecord::Base def runners_registration_token ensure_runners_registration_token! end + + def health_check_access_token + ensure_health_check_access_token! + end end diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb index 44b090260e7..967ffd46db0 100644 --- a/app/models/audit_event.rb +++ b/app/models/audit_event.rb @@ -1,17 +1,3 @@ -# == Schema Information -# -# Table name: audit_events -# -# id :integer not null, primary key -# author_id :integer not null -# type :string not null -# entity_id :integer not null -# entity_type :string not null -# details :text -# created_at :datetime -# updated_at :datetime -# - class AuditEvent < ActiveRecord::Base serialize :details, Hash diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb new file mode 100644 index 00000000000..59c7d87f5df --- /dev/null +++ b/app/models/award_emoji.rb @@ -0,0 +1,26 @@ +class AwardEmoji < ActiveRecord::Base + DOWNVOTE_NAME = "thumbsdown".freeze + UPVOTE_NAME = "thumbsup".freeze + + include Participable + + belongs_to :awardable, polymorphic: true + belongs_to :user + + validates :awardable, :user, presence: true + validates :name, presence: true, inclusion: { in: Emoji.emojis_names } + validates :name, uniqueness: { scope: [:user, :awardable_type, :awardable_id] } + + participant :user + + scope :downvotes, -> { where(name: DOWNVOTE_NAME) } + scope :upvotes, -> { where(name: UPVOTE_NAME) } + + def downvote? + self.name == DOWNVOTE_NAME + end + + def upvote? + self.name == UPVOTE_NAME + end +end diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index 075ac733bfc..61498140f27 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -1,17 +1,3 @@ -# == Schema Information -# -# Table name: broadcast_messages -# -# id :integer not null, primary key -# message :text not null -# starts_at :datetime -# ends_at :datetime -# created_at :datetime -# updated_at :datetime -# color :string -# font :string -# - class BroadcastMessage < ActiveRecord::Base include Sortable diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 4bc3a225e2c..6a64ca451f7 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -1,40 +1,3 @@ -# == Schema Information -# -# Table name: ci_builds -# -# id :integer not null, primary key -# project_id :integer -# status :string -# finished_at :datetime -# trace :text -# created_at :datetime -# updated_at :datetime -# started_at :datetime -# runner_id :integer -# coverage :float -# commit_id :integer -# commands :text -# job_id :integer -# name :string -# deploy :boolean default(FALSE) -# options :text -# allow_failure :boolean default(FALSE), not null -# stage :string -# trigger_request_id :integer -# stage_idx :integer -# tag :boolean -# ref :string -# user_id :integer -# type :string -# target_url :string -# description :string -# artifacts_file :text -# gl_project_id :integer -# artifacts_metadata :text -# erased_by_id :integer -# erased_at :datetime -# - module Ci class Build < CommitStatus belongs_to :runner, class_name: 'Ci::Runner' @@ -82,14 +45,15 @@ module Ci new_build.options = build.options new_build.commands = build.commands new_build.tag_list = build.tag_list - new_build.gl_project_id = build.gl_project_id - new_build.commit_id = build.commit_id + new_build.project = build.project + new_build.pipeline = build.pipeline new_build.name = build.name new_build.allow_failure = build.allow_failure new_build.stage = build.stage new_build.stage_idx = build.stage_idx new_build.trigger_request = build.trigger_request new_build.save + MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build) new_build end end @@ -102,7 +66,7 @@ module Ci # We use around_transition to create builds for next stage as soon as possible, before the `after_*` is executed around_transition any => [:success, :failed, :canceled] do |build, block| block.call - build.commit.create_next_builds(build) if build.commit + build.pipeline.create_next_builds(build) if build.pipeline end after_transition any => [:success, :failed, :canceled] do |build| @@ -116,7 +80,7 @@ module Ci end def retried? - !self.commit.statuses.latest.include?(self) + !self.pipeline.statuses.latest.include?(self) end def retry @@ -125,15 +89,19 @@ module Ci def depends_on_builds # Get builds of the same type - latest_builds = self.commit.builds.latest + latest_builds = self.pipeline.builds.latest # Return builds from previous stages latest_builds.where('stage_idx < ?', stage_idx) end def trace_html - html = Ci::Ansi2html::convert(trace) if trace.present? - html || '' + trace_with_state[:html] || '' + end + + def trace_with_state(state = nil) + trace_with_state = Ci::Ansi2html::convert(trace, state) if trace.present? + trace_with_state || {} end def timeout @@ -146,16 +114,16 @@ module Ci def merge_request merge_requests = MergeRequest.includes(:merge_request_diff) - .where(source_branch: ref, source_project_id: commit.gl_project_id) + .where(source_branch: ref, source_project_id: pipeline.gl_project_id) .reorder(iid: :asc) merge_requests.find do |merge_request| - merge_request.commits.any? { |ci| ci.id == commit.sha } + merge_request.commits.any? { |ci| ci.id == pipeline.sha } end end def project_id - commit.project.id + pipeline.project_id end def project_name @@ -226,7 +194,7 @@ module Ci def trace_length if raw_trace - raw_trace.length + raw_trace.bytesize else 0 end @@ -238,7 +206,7 @@ module Ci end def recreate_trace_dir - unless Dir.exists?(dir_to_trace) + unless Dir.exist?(dir_to_trace) FileUtils.mkdir_p(dir_to_trace) end end @@ -248,7 +216,7 @@ module Ci recreate_trace_dir File.truncate(path_to_trace, offset) if File.exist?(path_to_trace) - File.open(path_to_trace, 'a') do |f| + File.open(path_to_trace, 'ab') do |f| f.write(trace_part) end end @@ -318,14 +286,20 @@ module Ci project.runners_token end - def valid_token? token + def valid_token?(token) project.valid_runners_token? token end def can_be_served?(runner) + return false unless has_tags? || runner.run_untagged? + (tag_list - runner.tag_list).empty? end + def has_tags? + tag_list.any? + end + def any_runners_online? project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) } end @@ -339,6 +313,7 @@ module Ci build_data = Gitlab::BuildDataBuilder.build(self) project.execute_hooks(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks) + project.running_or_pending_build_count(force: true) end def artifacts? @@ -385,8 +360,8 @@ module Ci end def global_yaml_variables - if commit.config_processor - commit.config_processor.global_variables.map do |key, value| + if pipeline.config_processor + pipeline.config_processor.global_variables.map do |key, value| { key: key, value: value, public: true } end else @@ -395,8 +370,8 @@ module Ci end def job_yaml_variables - if commit.config_processor - commit.config_processor.job_variables(name).map do |key, value| + if pipeline.config_processor + pipeline.config_processor.job_variables(name).map do |key, value| { key: key, value: value, public: true } end else diff --git a/app/models/ci/commit.rb b/app/models/ci/pipeline.rb index 4ac4e0fb8b2..d780467034e 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/pipeline.rb @@ -1,36 +1,14 @@ -# == Schema Information -# -# Table name: ci_commits -# -# id :integer not null, primary key -# project_id :integer -# ref :string -# sha :string -# before_sha :string -# push_data :text -# created_at :datetime -# updated_at :datetime -# tag :boolean default(FALSE) -# yaml_errors :text -# committed_at :datetime -# gl_project_id :integer -# status :string -# started_at :datetime -# finished_at :datetime -# duration :integer -# - module Ci - class Commit < ActiveRecord::Base + class Pipeline < ActiveRecord::Base extend Ci::Model include Statuseable - belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id - has_many :statuses, class_name: 'CommitStatus' - has_many :builds, class_name: 'Ci::Build' - has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' + self.table_name = 'ci_commits' - delegate :stages, to: :statuses + belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id + has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id + has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id + has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id validates_presence_of :sha validates_presence_of :status @@ -44,7 +22,8 @@ module Ci end def self.stages - CommitStatus.where(commit: all).stages + # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries + CommitStatus.where(pipeline: pluck(:id)).stages end def project_id @@ -70,7 +49,7 @@ module Ci end def short_sha - Ci::Commit.truncate_sha(sha) + Ci::Pipeline.truncate_sha(sha) end def commit_data @@ -89,6 +68,29 @@ module Ci end end + def cancelable? + builds.running_or_pending.any? + end + + def cancel_running + builds.running_or_pending.each(&:cancel) + end + + def retry_failed + builds.latest.failed.select(&:retryable?).each(&:retry) + end + + def latest? + return false unless ref + commit = project.commit(ref) + return false unless commit + commit.sha == sha + end + + def triggered? + trigger_requests.any? + end + def create_builds(user, trigger_request = nil) return unless config_processor config_processor.stages.any? do |stage| @@ -159,6 +161,10 @@ module Ci git_commit_message =~ /(\[ci skip\])/ if git_commit_message end + def notes + Note.for_commit_id(sha) + end + private def update_state diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index add59a08892..adb65292208 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -1,28 +1,10 @@ -# == Schema Information -# -# Table name: ci_runners -# -# id :integer not null, primary key -# token :string -# created_at :datetime -# updated_at :datetime -# description :string -# contacted_at :datetime -# active :boolean default(TRUE), not null -# is_shared :boolean default(FALSE) -# name :string -# version :string -# revision :string -# platform :string -# architecture :string -# - module Ci class Runner < ActiveRecord::Base extend Ci::Model LAST_CONTACT_TIME = 5.minutes.ago - AVAILABLE_SCOPES = ['specific', 'shared', 'active', 'paused', 'online'] + AVAILABLE_SCOPES = %w[specific shared active paused online] + FORM_EDITABLE = %i[description tag_list active run_untagged] has_many :builds, class_name: 'Ci::Build' has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject' @@ -44,6 +26,8 @@ module Ci .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) end + validate :tag_constraints + acts_as_taggable # Searches for runners matching the given query. @@ -76,7 +60,7 @@ module Ci end def display_name - return short_sha unless !description.blank? + return short_sha if description.blank? description end @@ -114,5 +98,18 @@ module Ci def short_sha token[0...8] if token end + + def has_tags? + tag_list.any? + end + + private + + def tag_constraints + unless has_tags? || run_untagged? + errors.add(:tags_list, + 'can not be empty when runner is not allowed to pick untagged jobs') + end + end end end diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb index 7b16f207a26..4b44ffa886e 100644 --- a/app/models/ci/runner_project.rb +++ b/app/models/ci/runner_project.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: ci_runner_projects -# -# id :integer not null, primary key -# runner_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# gl_project_id :integer -# - module Ci class RunnerProject < ActiveRecord::Base extend Ci::Model diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index 4f3f4d79fac..a0b19b51a12 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -1,16 +1,3 @@ -# == Schema Information -# -# Table name: ci_triggers -# -# id :integer not null, primary key -# token :string -# project_id :integer -# deleted_at :datetime -# created_at :datetime -# updated_at :datetime -# gl_project_id :integer -# - module Ci class Trigger < ActiveRecord::Base extend Ci::Model diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb index 9973d2e5ade..b69ae37668c 100644 --- a/app/models/ci/trigger_request.rb +++ b/app/models/ci/trigger_request.rb @@ -1,21 +1,9 @@ -# == Schema Information -# -# Table name: ci_trigger_requests -# -# id :integer not null, primary key -# trigger_id :integer not null -# variables :text -# created_at :datetime -# updated_at :datetime -# commit_id :integer -# - module Ci class TriggerRequest < ActiveRecord::Base extend Ci::Model belongs_to :trigger, class_name: 'Ci::Trigger' - belongs_to :commit, class_name: 'Ci::Commit' + belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id has_many :builds, class_name: 'Ci::Build' serialize :variables diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 4229fe085a1..f8d5d4486fd 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -1,17 +1,3 @@ -# == Schema Information -# -# Table name: ci_variables -# -# id :integer not null, primary key -# project_id :integer -# key :string -# value :text -# encrypted_value :text -# encrypted_value_salt :string -# encrypted_value_iv :string -# gl_project_id :integer -# - module Ci class Variable < ActiveRecord::Base extend Ci::Model @@ -25,6 +11,9 @@ module Ci format: { with: /\A[a-zA-Z0-9_]+\z/, message: "can contain only letters, digits and '_'." } - attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base + attr_encrypted :value, + mode: :per_attribute_iv_and_salt, + key: Gitlab::Application.secrets.db_key_base, + algorithm: 'aes-256-cbc' end end diff --git a/app/models/commit.rb b/app/models/commit.rb index 562c3ed15b2..d69d518fadd 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -8,7 +8,10 @@ class Commit include StaticModel attr_mentionable :safe_message, pipeline: :single_line - participant :author, :committer, :notes + + participant :author + participant :committer + participant :notes_with_associations attr_accessor :project @@ -194,6 +197,10 @@ class Commit project.notes.for_commit_id(self.id) end + def notes_with_associations + notes.includes(:author) + end + def method_missing(m, *args, &block) @raw.send(m, *args, &block) end @@ -207,19 +214,19 @@ class Commit @raw.short_id(7) end - def ci_commits - @ci_commits ||= project.ci_commits.where(sha: sha) + def pipelines + @pipeline ||= project.pipelines.where(sha: sha) end def status return @status if defined?(@status) - @status ||= ci_commits.status + @status ||= pipelines.status end def revert_branch_name "revert-#{short_id}" end - + def cherry_pick_branch_name project.repository.next_branch("cherry-pick-#{short_id}", mild: true) end @@ -251,11 +258,13 @@ class Commit end def has_been_reverted?(current_user = nil, noteable = self) - Gitlab::ReferenceExtractor.lazily do - noteable.notes.system.flat_map do |note| - note.all_references(current_user).commits - end - end.any? { |commit_ref| commit_ref.reverts_commit?(self) } + ext = all_references(current_user) + + noteable.notes_with_associations.system.each do |note| + note.all_references(current_user, extractor: ext) + end + + ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self) } end def change_type_title diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 51673897d98..4066958f67c 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -62,7 +62,7 @@ class CommitRange def initialize(range_string, project) @project = project - range_string.strip! + range_string = range_string.strip unless range_string =~ /\A#{PATTERN}\z/ raise ArgumentError, "invalid CommitRange string format: #{range_string}" diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 1260c448de3..e53c483b904 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -1,57 +1,21 @@ -# == Schema Information -# -# Table name: ci_builds -# -# id :integer not null, primary key -# project_id :integer -# status :string -# finished_at :datetime -# trace :text -# created_at :datetime -# updated_at :datetime -# started_at :datetime -# runner_id :integer -# coverage :float -# commit_id :integer -# commands :text -# job_id :integer -# name :string -# deploy :boolean default(FALSE) -# options :text -# allow_failure :boolean default(FALSE), not null -# stage :string -# trigger_request_id :integer -# stage_idx :integer -# tag :boolean -# ref :string -# user_id :integer -# type :string -# target_url :string -# description :string -# artifacts_file :text -# gl_project_id :integer -# artifacts_metadata :text -# erased_by_id :integer -# erased_at :datetime -# - class CommitStatus < ActiveRecord::Base include Statuseable self.table_name = 'ci_builds' belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id - belongs_to :commit, class_name: 'Ci::Commit', touch: true + belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true belongs_to :user - validates :commit, presence: true + validates :pipeline, presence: true validates_presence_of :name alias_attribute :author, :user scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) } - scope :ordered, -> { order(:ref, :stage_idx, :name) } + scope :retried, -> { where.not(id: latest) } + scope :ordered, -> { order(:name) } scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } state_machine :status, initial: :pending do @@ -80,24 +44,30 @@ class CommitStatus < ActiveRecord::Base end after_transition [:pending, :running] => :success do |commit_status| - MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.commit.project, nil).trigger(commit_status) + MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status) + end + + after_transition any => :failed do |commit_status| + MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status) end end - delegate :sha, :short_sha, to: :commit + delegate :sha, :short_sha, to: :pipeline def before_sha - commit.before_sha || Gitlab::Git::BLANK_SHA + pipeline.before_sha || Gitlab::Git::BLANK_SHA end def self.stages - order_by = 'max(stage_idx)' - group('stage').order(order_by).pluck(:stage, order_by).map(&:first).compact + # We group by stage name, but order stages by theirs' index + unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage') end def self.stages_status - all.stages.inject({}) do |h, stage| - h[stage] = all.where(stage: stage).status + # We execute subquery for each stage to calculate a stage status + statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql) + statuses.inject({}) do |h, k| + h[k.first] = k.last h end end diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb new file mode 100644 index 00000000000..aa4b4201250 --- /dev/null +++ b/app/models/concerns/awardable.rb @@ -0,0 +1,81 @@ +module Awardable + extend ActiveSupport::Concern + + included do + has_many :award_emoji, as: :awardable, dependent: :destroy + + if self < Participable + participant :award_emoji + end + end + + module ClassMethods + def order_upvotes_desc + order_votes_desc(AwardEmoji::UPVOTE_NAME) + end + + def order_downvotes_desc + order_votes_desc(AwardEmoji::DOWNVOTE_NAME) + end + + def order_votes_desc(emoji_name) + awardable_table = self.arel_table + awards_table = AwardEmoji.arel_table + + join_clause = awardable_table.join(awards_table, Arel::Nodes::OuterJoin).on( + awards_table[:awardable_id].eq(awardable_table[:id]).and( + awards_table[:awardable_type].eq(self.name).and( + awards_table[:name].eq(emoji_name) + ) + ) + ).join_sources + + joins(join_clause).group(awardable_table[:id]).reorder("COUNT(award_emoji.id) DESC") + end + end + + def grouped_awards(with_thumbs: true) + awards = award_emoji.group_by(&:name) + + if with_thumbs + awards[AwardEmoji::UPVOTE_NAME] ||= [] + awards[AwardEmoji::DOWNVOTE_NAME] ||= [] + end + + awards + end + + def downvotes + award_emoji.downvotes.count + end + + def upvotes + award_emoji.upvotes.count + end + + def emoji_awardable? + true + end + + def awarded_emoji?(emoji_name, current_user) + award_emoji.where(name: emoji_name, user: current_user).exists? + end + + def create_award_emoji(name, current_user) + return unless emoji_awardable? + + award_emoji.create(name: name, user: current_user) + end + + def remove_award_emoji(name, current_user) + award_emoji.where(name: name, user: current_user).destroy_all + end + + def toggle_award_emoji(emoji_name, current_user) + if awarded_emoji?(emoji_name, current_user) + remove_award_emoji(emoji_name, current_user) + else + create_award_emoji(emoji_name, current_user) + end + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 2e4efc4e8d8..0ccd3474b81 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -10,13 +10,19 @@ module Issuable include Mentionable include Subscribable include StripAttribute + include Awardable included do belongs_to :author, class_name: "User" belongs_to :assignee, class_name: "User" belongs_to :updated_by, class_name: "User" belongs_to :milestone - has_many :notes, as: :noteable, dependent: :destroy + has_many :notes, as: :noteable, dependent: :destroy do + def authors_loaded? + # We check first if we're loaded to not load unnecesarily. + loaded? && to_a.all? { |note| note.association(:author).loaded? } + end + end has_many :label_links, as: :target, dependent: :destroy has_many :labels, through: :label_links has_many :todos, as: :target, dependent: :destroy @@ -31,18 +37,22 @@ module Issuable scope :unassigned, -> { where("assignee_id IS NULL") } scope :of_projects, ->(ids) { where(project_id: ids) } scope :of_milestones, ->(ids) { where(milestone_id: ids) } + scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) } scope :opened, -> { with_state(:opened, :reopened) } scope :only_opened, -> { with_state(:opened) } scope :only_reopened, -> { with_state(:reopened) } scope :closed, -> { with_state(:closed) } - scope :order_milestone_due_desc, -> { outer_join_milestone.reorder('milestones.due_date IS NULL ASC, milestones.due_date DESC, milestones.id DESC') } - scope :order_milestone_due_asc, -> { outer_join_milestone.reorder('milestones.due_date IS NULL ASC, milestones.due_date ASC, milestones.id ASC') } - scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } + scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") } + scope :order_milestone_due_desc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC') } + scope :order_milestone_due_asc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC') } + + scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :join_project, -> { joins(:project) } + scope :inc_notes_with_associations, -> { includes(notes: :author) } scope :references_project, -> { references(:project) } scope :non_archived, -> { join_project.where(projects: { archived: false }) } - scope :outer_join_milestone, -> { joins("LEFT OUTER JOIN milestones ON milestones.id = #{table_name}.milestone_id") } + delegate :name, :email, @@ -56,11 +66,23 @@ module Issuable prefix: true attr_mentionable :title, pipeline: :single_line - attr_mentionable :description, cache: true - participant :author, :assignee, :notes_with_associations + attr_mentionable :description + + participant :author + participant :assignee + participant :notes_with_associations + strip_attributes :title acts_as_paranoid + + after_save :update_assignee_cache_counts, if: :assignee_id_changed? + + def update_assignee_cache_counts + # make sure we flush the cache for both the old *and* new assignee + User.find(assignee_id_was).update_cache_counts if assignee_id_was + assignee.update_cache_counts if assignee + end end module ClassMethods @@ -89,46 +111,60 @@ module Issuable where(t[:title].matches(pattern).or(t[:description].matches(pattern))) end - def sort(method) + def sort(method, excluded_labels: []) case method.to_s when 'milestone_due_asc' then order_milestone_due_asc when 'milestone_due_desc' then order_milestone_due_desc when 'downvotes_desc' then order_downvotes_desc when 'upvotes_desc' then order_upvotes_desc + when 'priority' then order_labels_priority(excluded_labels: excluded_labels) else order_by(method) end end - def order_downvotes_desc - order_votes_desc('thumbsdown') + def order_labels_priority(excluded_labels: []) + select("#{table_name}.*, (#{highest_label_priority(excluded_labels).to_sql}) AS highest_priority"). + group(arel_table[:id]). + reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')) end - def order_upvotes_desc - order_votes_desc('thumbsup') + def with_label(title, sort = nil) + if title.is_a?(Array) && title.size > 1 + joins(:labels).where(labels: { title: title }).group(*grouping_columns(sort)).having("COUNT(DISTINCT labels.title) = #{title.size}") + else + joins(:labels).where(labels: { title: title }) + end end - def order_votes_desc(award_emoji_name) - issuable_table = self.arel_table - note_table = Note.arel_table - - join_clause = issuable_table.join(note_table, Arel::Nodes::OuterJoin).on( - note_table[:noteable_id].eq(issuable_table[:id]).and( - note_table[:noteable_type].eq(self.name).and( - note_table[:is_award].eq(true).and(note_table[:note].eq(award_emoji_name)) - ) - ) - ).join_sources + # Includes table keys in group by clause when sorting + # preventing errors in postgres + # + # Returns an array of arel columns + def grouping_columns(sort) + grouping_columns = [arel_table[:id]] + + if ["milestone_due_desc", "milestone_due_asc"].include?(sort) + milestone_table = Milestone.arel_table + grouping_columns << milestone_table[:id] + grouping_columns << milestone_table[:due_date] + end - joins(join_clause).group(issuable_table[:id]).reorder("COUNT(notes.id) DESC") + grouping_columns end - def with_label(title) - if title.is_a?(Array) && title.count > 1 - joins(:labels).where(labels: { title: title }).group('issues.id').having("count(distinct labels.title) = #{title.count}") - else - joins(:labels).where(labels: { title: title }) - end + private + + def highest_label_priority(excluded_labels) + query = Label.select(Label.arel_table[:priority].minimum). + joins(:label_links). + where(label_links: { target_type: name }). + where("label_links.target_id = #{table_name}.id"). + reorder(nil) + + query.where.not(title: excluded_labels) if excluded_labels.present? + + query end end @@ -140,10 +176,6 @@ module Issuable today? && created_at == updated_at end - def is_assigned? - !!assignee_id - end - def is_being_reassigned? assignee_id_changed? end @@ -152,12 +184,14 @@ module Issuable opened? || reopened? end - def downvotes - notes.awards.where(note: "thumbsdown").count - end - - def upvotes - notes.awards.where(note: "thumbsup").count + def user_notes_count + if notes.loaded? + # Use the in-memory association to select and count to avoid hitting the db + notes.to_a.count { |note| !note.system? } + else + # do the count query + notes.user.count + end end def subscribed_without_subscriptions?(user) @@ -178,6 +212,10 @@ module Issuable hook_data end + def labels_array + labels.to_a + end + def label_names labels.order('title ASC').pluck(:title) end @@ -213,7 +251,13 @@ module Issuable end def notes_with_associations - notes.includes(:author, :project) + # If A has_many Bs, and B has_many Cs, and you do + # `A.includes(b: :c).each { |a| a.b.includes(:c) }`, sadly ActiveRecord + # will do the inclusion again. So, we check if all notes in the relation + # already have their authors loaded (possibly because the scope + # `inc_notes_with_associations` was used) and skip the inclusion if that's + # the case. + notes.authors_loaded? ? notes : notes.includes(:author) end def updated_tasks diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 98f71ae8cb0..f00b5b8497c 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -23,7 +23,7 @@ module Mentionable included do if self < Participable - participant ->(current_user) { mentioned_users(current_user) } + participant -> (user, ext) { all_references(user, extractor: ext) } end end @@ -43,23 +43,22 @@ module Mentionable self end - def all_references(current_user = self.author, text = nil) - ext = Gitlab::ReferenceExtractor.new(self.project, current_user, self.author) + def all_references(current_user = nil, text = nil, extractor: nil) + extractor ||= Gitlab::ReferenceExtractor. + new(project, current_user || author) if text - ext.analyze(text) + extractor.analyze(text, author: author) else self.class.mentionable_attrs.each do |attr, options| - text = send(attr) + text = __send__(attr) + options = options.merge(cache_key: [self, attr], author: author) - context = options.dup - context[:cache_key] = [self, attr] if context.delete(:cache) && self.persisted? - - ext.analyze(text, context) + extractor.analyze(text, options) end end - ext + extractor end def mentioned_users(current_user = nil) diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index fc6f83b918b..9056722f45e 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -3,8 +3,6 @@ # Contains functionality related to objects that can have participants, such as # an author, an assignee and people mentioned in its description or comments. # -# Used by Issue, Note, MergeRequest, Snippet and Commit. -# # Usage: # # class Issue < ActiveRecord::Base @@ -12,22 +10,36 @@ # # # ... # -# participant :author, :assignee, :notes, ->(current_user) { mentioned_users(current_user) } +# participant :author +# participant :assignee +# participant :notes +# +# participant -> (current_user, ext) do +# ext.analyze('...') +# end # end # # issue = Issue.last # users = issue.participants -# # `users` will contain the issue's author, its assignee, -# # all users returned by its #mentioned_users method, -# # as well as all participants to all of the issue's notes, -# # since Note implements Participable as well. -# module Participable extend ActiveSupport::Concern module ClassMethods - def participant(*attrs) - participant_attrs.concat(attrs) + # Adds a list of participant attributes. Attributes can either be symbols or + # Procs. + # + # When using a Proc instead of a Symbol the Proc will be given two + # arguments: + # + # 1. The current user (as an instance of User) + # 2. An instance of `Gitlab::ReferenceExtractor` + # + # It is expected that a Proc populates the given reference extractor + # instance with data. The return value of the Proc is ignored. + # + # attr - The name of the attribute or a Proc + def participant(attr) + participant_attrs << attr end def participant_attrs @@ -35,42 +47,42 @@ module Participable end end - # Be aware that this method makes a lot of sql queries. - # Save result into variable if you are going to reuse it inside same request - def participants(current_user = self.author) - participants = - Gitlab::ReferenceExtractor.lazily do - self.class.participant_attrs.flat_map do |attr| - value = - if attr.respond_to?(:call) - instance_exec(current_user, &attr) - else - send(attr) - end + # Returns the users participating in a discussion. + # + # This method processes attributes of objects in breadth-first order. + # + # Returns an Array of User instances. + def participants(current_user = nil) + current_user ||= author + ext = Gitlab::ReferenceExtractor.new(project, current_user) + participants = Set.new + process = [self] - participants_for(value, current_user) - end.compact.uniq - end + until process.empty? + source = process.pop - unless Gitlab::ReferenceExtractor.lazy? - participants.select! do |user| - user.can?(:read_project, project) + case source + when User + participants << source + when Participable + source.class.participant_attrs.each do |attr| + if attr.respond_to?(:call) + source.instance_exec(current_user, ext, &attr) + else + process << source.__send__(attr) + end + end + when Enumerable, ActiveRecord::Relation + # This uses reverse_each so we can use "pop" to get the next value to + # process (in order). Using unshift instead of pop would require + # moving all Array values one index to the left (which can be + # expensive). + source.reverse_each { |obj| process << obj } end end - participants - end - - private + participants.merge(ext.users) - def participants_for(value, current_user = nil) - case value - when User, Banzai::LazyReference - [value] - when Enumerable, ActiveRecord::Relation - value.flat_map { |v| participants_for(v, current_user) } - when Participable - value.participants(current_user) - end + Ability.users_that_can_read_project(participants.to_a, project) end end diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb index d5a881b2445..083257f1005 100644 --- a/app/models/concerns/subscribable.rb +++ b/app/models/concerns/subscribable.rb @@ -36,6 +36,12 @@ module Subscribable update(subscribed: !subscribed?(user)) end + def subscribe(user) + subscriptions. + find_or_initialize_by(user_id: user.id). + update(subscribed: true) + end + def unsubscribe(user) subscriptions. find_or_initialize_by(user_id: user.id). diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index 43cf625f770..2c525d4cd7a 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: keys -# -# id :integer not null, primary key -# user_id :integer -# created_at :datetime -# updated_at :datetime -# key :text -# title :string -# type :string -# fingerprint :string -# public :boolean default(FALSE), not null -# - class DeployKey < Key has_many :deploy_keys_projects, dependent: :destroy has_many :projects, through: :deploy_keys_projects diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb index 18db521741f..ae8486bd9ac 100644 --- a/app/models/deploy_keys_project.rb +++ b/app/models/deploy_keys_project.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: deploy_keys_projects -# -# id :integer not null, primary key -# deploy_key_id :integer not null -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# - class DeployKeysProject < ActiveRecord::Base belongs_to :project belongs_to :deploy_key diff --git a/app/models/email.rb b/app/models/email.rb index eae2472f337..32a412ab878 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: emails -# -# id :integer not null, primary key -# user_id :integer not null -# email :string not null -# created_at :datetime -# updated_at :datetime -# - class Email < ActiveRecord::Base include Sortable diff --git a/app/models/event.rb b/app/models/event.rb index 25c7c3e6dc7..716039fb54b 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,19 +1,3 @@ -# == Schema Information -# -# Table name: events -# -# id :integer not null, primary key -# target_type :string -# target_id :integer -# title :string -# data :text -# project_id :integer -# created_at :datetime -# updated_at :datetime -# action :integer -# author_id :integer -# - class Event < ActiveRecord::Base include Sortable default_scope { where.not(author_id: nil) } @@ -96,7 +80,7 @@ class Event < ActiveRecord::Base end def target_title - target.title if target && target.respond_to?(:title) + target.try(:title) end def created? @@ -282,28 +266,20 @@ class Event < ActiveRecord::Base branch? && project.default_branch != branch_name end - def note_commit_id - target.commit_id - end - def target_iid target.respond_to?(:iid) ? target.iid : target_id end - def note_short_commit_id - Commit.truncate_sha(note_commit_id) - end - - def note_commit? - target.noteable_type == "Commit" + def commit_note? + target.for_commit? end def issue_note? - note? && target && target.noteable_type == "Issue" + note? && target && target.for_issue? end - def note_project_snippet? - target.noteable_type == "Snippet" + def project_snippet_note? + target.for_snippet? end def note_target @@ -311,19 +287,22 @@ class Event < ActiveRecord::Base end def note_target_id - if note_commit? + if commit_note? target.commit_id else target.noteable_id.to_s end end - def note_target_iid - if note_target.respond_to?(:iid) - note_target.iid + def note_target_reference + return unless note_target + + # Commit#to_reference returns the full SHA, but we want the short one here + if commit_note? + note_target.short_id else - note_target_id - end.to_s + note_target.to_reference + end end def note_target_type diff --git a/app/models/forked_project_link.rb b/app/models/forked_project_link.rb index 9b0c6263a96..9803bae0bee 100644 --- a/app/models/forked_project_link.rb +++ b/app/models/forked_project_link.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: forked_project_links -# -# id :integer not null, primary key -# forked_to_project_id :integer not null -# forked_from_project_id :integer not null -# created_at :datetime -# updated_at :datetime -# - class ForkedProjectLink < ActiveRecord::Base belongs_to :forked_to_project, class_name: Project belongs_to :forked_from_project, class_name: Project diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb index d4afd8cbe84..fa54e3540d0 100644 --- a/app/models/generic_commit_status.rb +++ b/app/models/generic_commit_status.rb @@ -1,40 +1,3 @@ -# == Schema Information -# -# Table name: ci_builds -# -# id :integer not null, primary key -# project_id :integer -# status :string -# finished_at :datetime -# trace :text -# created_at :datetime -# updated_at :datetime -# started_at :datetime -# runner_id :integer -# coverage :float -# commit_id :integer -# commands :text -# job_id :integer -# name :string -# deploy :boolean default(FALSE) -# options :text -# allow_failure :boolean default(FALSE), not null -# stage :string -# trigger_request_id :integer -# stage_idx :integer -# tag :boolean -# ref :string -# user_id :integer -# type :string -# target_url :string -# description :string -# artifacts_file :text -# gl_project_id :integer -# artifacts_metadata :text -# erased_by_id :integer -# erased_at :datetime -# - class GenericCommitStatus < CommitStatus before_validation :set_default_values diff --git a/app/models/group.rb b/app/models/group.rb index cff76877958..aec92e335e6 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,20 +1,3 @@ -# == Schema Information -# -# Table name: namespaces -# -# id :integer not null, primary key -# name :string not null -# path :string not null -# owner_id :integer -# created_at :datetime -# updated_at :datetime -# type :string -# description :string default(""), not null -# avatar :string -# share_with_group_lock :boolean default(FALSE) -# visibility_level :integer default(20), not null -# - require 'carrierwave/orm/activerecord' class Group < Namespace diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index 2b8f34a0568..ba42a8eeb70 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -1,25 +1,3 @@ -# == Schema Information -# -# Table name: web_hooks -# -# id :integer not null, primary key -# url :string(2000) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string default("ProjectHook") -# service_id :integer -# push_events :boolean default(TRUE), not null -# issues_events :boolean default(FALSE), not null -# merge_requests_events :boolean default(FALSE), not null -# tag_push_events :boolean default(FALSE) -# note_events :boolean default(FALSE), not null -# enable_ssl_verification :boolean default(TRUE) -# build_events :boolean default(FALSE), not null -# wiki_page_events :boolean default(FALSE), not null -# token :string -# - class ProjectHook < WebHook belongs_to :project diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb index 0e176de5ef8..eef24052a06 100644 --- a/app/models/hooks/service_hook.rb +++ b/app/models/hooks/service_hook.rb @@ -1,25 +1,3 @@ -# == Schema Information -# -# Table name: web_hooks -# -# id :integer not null, primary key -# url :string(2000) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string default("ProjectHook") -# service_id :integer -# push_events :boolean default(TRUE), not null -# issues_events :boolean default(FALSE), not null -# merge_requests_events :boolean default(FALSE), not null -# tag_push_events :boolean default(FALSE) -# note_events :boolean default(FALSE), not null -# enable_ssl_verification :boolean default(TRUE) -# build_events :boolean default(FALSE), not null -# wiki_page_events :boolean default(FALSE), not null -# token :string -# - class ServiceHook < WebHook belongs_to :service diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index ad508cbbcb8..777bad1e724 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -1,25 +1,3 @@ -# == Schema Information -# -# Table name: web_hooks -# -# id :integer not null, primary key -# url :string(2000) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string default("ProjectHook") -# service_id :integer -# push_events :boolean default(TRUE), not null -# issues_events :boolean default(FALSE), not null -# merge_requests_events :boolean default(FALSE), not null -# tag_push_events :boolean default(FALSE) -# note_events :boolean default(FALSE), not null -# enable_ssl_verification :boolean default(TRUE) -# build_events :boolean default(FALSE), not null -# wiki_page_events :boolean default(FALSE), not null -# token :string -# - class SystemHook < WebHook def async_execute(data, hook_name) Sidekiq::Client.enqueue(SystemHookWorker, id, data, hook_name) diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 8e58c9583ab..8b87b6c3d64 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -1,25 +1,3 @@ -# == Schema Information -# -# Table name: web_hooks -# -# id :integer not null, primary key -# url :string(2000) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# type :string default("ProjectHook") -# service_id :integer -# push_events :boolean default(TRUE), not null -# issues_events :boolean default(FALSE), not null -# merge_requests_events :boolean default(FALSE), not null -# tag_push_events :boolean default(FALSE) -# note_events :boolean default(FALSE), not null -# enable_ssl_verification :boolean default(TRUE) -# build_events :boolean default(FALSE), not null -# wiki_page_events :boolean default(FALSE), not null -# token :string -# - class WebHook < ActiveRecord::Base include Sortable include HTTParty @@ -60,7 +38,7 @@ class WebHook < ActiveRecord::Base basic_auth: auth) end - [(response.code >= 200 && response.code < 300), ActionView::Base.full_sanitizer.sanitize(response.to_s)] + [response.code, response.to_s] rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e logger.error("WebHook Error => #{e}") [false, e.to_s] diff --git a/app/models/identity.rb b/app/models/identity.rb index ef4d5f99091..3bacc450e6e 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: identities -# -# id :integer not null, primary key -# extern_uid :string -# provider :string -# user_id :integer -# created_at :datetime -# updated_at :datetime -# - class Identity < ActiveRecord::Base include Sortable include CaseSensitivity diff --git a/app/models/issue.rb b/app/models/issue.rb index abaa509707c..235922710ad 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: issues -# -# id :integer not null, primary key -# title :string -# assignee_id :integer -# author_id :integer -# project_id :integer -# created_at :datetime -# updated_at :datetime -# position :integer default(0) -# branch_name :string -# description :text -# milestone_id :integer -# state :string -# iid :integer -# updated_by_id :integer -# moved_to_id :integer -# confidential :boolean default(FALSE) -# deleted_at :datetime -# due_date :date -# - require 'carrierwave/orm/activerecord' class Issue < ActiveRecord::Base @@ -99,10 +75,10 @@ class Issue < ActiveRecord::Base @link_reference_pattern ||= super("issues", /(?<issue>\d+)/) end - def self.sort(method) + def self.sort(method, excluded_labels: []) case method.to_s when 'due_date_asc' then order_due_date_asc - when 'due_date_desc' then order_due_date_desc + when 'due_date_desc' then order_due_date_desc else super end @@ -119,14 +95,13 @@ class Issue < ActiveRecord::Base end def referenced_merge_requests(current_user = nil) - @referenced_merge_requests ||= {} - @referenced_merge_requests[current_user] ||= begin - Gitlab::ReferenceExtractor.lazily do - [self, *notes].flat_map do |note| - note.all_references(current_user).merge_requests - end - end.sort_by(&:iid).uniq + ext = all_references(current_user) + + notes_with_associations.each do |object| + object.all_references(current_user, extractor: ext) end + + ext.merge_requests.sort_by(&:iid) end # All branches containing the current issue's ID, except for @@ -163,9 +138,13 @@ class Issue < ActiveRecord::Base def closed_by_merge_requests(current_user = nil) return [] unless open? - notes.system.flat_map do |note| - note.all_references(current_user).merge_requests - end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) } + ext = all_references(current_user) + + notes.system.each do |note| + note.all_references(current_user, extractor: ext) + end + + ext.merge_requests.select { |mr| mr.open? && mr.closes_issue?(self) } end def moved? diff --git a/app/models/key.rb b/app/models/key.rb index b2b57849f8a..0532e84f47d 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: keys -# -# id :integer not null, primary key -# user_id :integer -# created_at :datetime -# updated_at :datetime -# key :text -# title :string -# type :string -# fingerprint :string -# public :boolean default(FALSE), not null -# - require 'digest/md5' class Key < ActiveRecord::Base @@ -41,7 +26,7 @@ class Key < ActiveRecord::Base end def publishable_key - #Removes anything beyond the keytype and key itself + # Removes anything beyond the keytype and key itself self.key.split[0..1].join(' ') end diff --git a/app/models/label.rb b/app/models/label.rb index 9a22398d952..49c352cc239 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -1,17 +1,3 @@ -# == Schema Information -# -# Table name: labels -# -# id :integer not null, primary key -# title :string -# color :string -# project_id :integer -# created_at :datetime -# updated_at :datetime -# template :boolean default(FALSE) -# description :string -# - class Label < ActiveRecord::Base include Referable include Subscribable @@ -40,10 +26,20 @@ class Label < ActiveRecord::Base format: { with: /\A[^&\?,]+\z/ }, uniqueness: { scope: :project_id } + before_save :nullify_priority + default_scope { order(title: :asc) } scope :templates, -> { where(template: true) } + def self.prioritized + where.not(priority: nil).reorder(:priority, :title) + end + + def self.unprioritized + where(priority: nil) + end + alias_attribute :name, :title def self.reference_prefix @@ -117,6 +113,10 @@ class Label < ActiveRecord::Base LabelsHelper::text_color_for_bg(self.color) end + def title=(value) + write_attribute(:title, Sanitize.clean(value.to_s)) if value.present? + end + private def label_format_reference(format = :id) @@ -128,4 +128,8 @@ class Label < ActiveRecord::Base id end end + + def nullify_priority + self.priority = nil if priority.blank? + end end diff --git a/app/models/label_link.rb b/app/models/label_link.rb index 7b8e872b6dd..47bd6eaf35f 100644 --- a/app/models/label_link.rb +++ b/app/models/label_link.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: label_links -# -# id :integer not null, primary key -# label_id :integer -# target_id :integer -# target_type :string -# created_at :datetime -# updated_at :datetime -# - class LabelLink < ActiveRecord::Base belongs_to :target, polymorphic: true belongs_to :label diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb new file mode 100644 index 00000000000..95fd510eb3a --- /dev/null +++ b/app/models/legacy_diff_note.rb @@ -0,0 +1,161 @@ +class LegacyDiffNote < Note + serialize :st_diff + + validates :line_code, presence: true, line_code: true + + before_create :set_diff + + class << self + def build_discussion_id(noteable_type, noteable_id, line_code, active = true) + [super(noteable_type, noteable_id), line_code, active].join("-") + end + end + + def diff_note? + true + end + + def legacy_diff_note? + true + end + + def discussion_id + @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code, active?) + end + + def diff_file_hash + line_code.split('_')[0] if line_code + end + + def diff_old_line + line_code.split('_')[1].to_i if line_code + end + + def diff_new_line + line_code.split('_')[2].to_i if line_code + end + + def diff + @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) + end + + def diff_file_path + diff.new_path.presence || diff.old_path + end + + def diff_lines + @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line) + end + + def diff_line + @diff_line ||= diff_lines.find { |line| generate_line_code(line) == self.line_code } + end + + def diff_line_text + diff_line.try(:text) + end + + def diff_line_type + diff_line.try(:type) + end + + def highlighted_diff_lines + Gitlab::Diff::Highlight.new(diff_lines).highlight + end + + def truncated_diff_lines + max_number_of_lines = 16 + prev_match_line = nil + prev_lines = [] + + highlighted_diff_lines.each do |line| + if line.type == "match" + prev_lines.clear + prev_match_line = line + else + prev_lines << line + + break if generate_line_code(line) == self.line_code + + prev_lines.shift if prev_lines.length >= max_number_of_lines + end + end + + prev_lines + end + + # Check if this note is part of an "active" discussion + # + # This will always return true for anything except MergeRequest noteables, + # which have special logic. + # + # If the note's current diff cannot be matched in the MergeRequest's current + # diff, it's considered inactive. + def active? + return @active if defined?(@active) + return true if for_commit? + return true unless self.diff + return false unless noteable + + noteable_diff = find_noteable_diff + + if noteable_diff + parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line) + + @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line_text } + else + @active = false + end + + @active + end + + def award_emoji_supported? + false + end + + private + + def find_diff + return nil unless noteable + return @diff if defined?(@diff) + + @diff = noteable.diffs(Commit.max_diff_options).find do |d| + d.new_path && Digest::SHA1.hexdigest(d.new_path) == diff_file_hash + end + end + + def set_diff + # First lets find notes with same diff + # before iterating over all mr diffs + diff = diff_for_line_code unless for_merge_request? + diff ||= find_diff + + self.st_diff = diff.to_hash if diff + end + + def diff_for_line_code + attributes = { + noteable_type: noteable_type, + line_code: line_code + } + + if for_commit? + attributes[:commit_id] = commit_id + else + attributes[:noteable_id] = noteable_id + end + + self.class.where(attributes).last.try(:diff) + end + + def generate_line_code(line) + Gitlab::Diff::LineCode.generate(diff_file_path, line.new_pos, line.old_pos) + end + + # Find the diff on noteable that matches our own + def find_noteable_diff + diffs = noteable.diffs(Commit.max_diff_options) + diffs.find { |d| d.new_path == self.diff.new_path } + end +end diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 927e764af92..18657c3e1c8 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: lfs_objects -# -# id :integer not null, primary key -# oid :string not null -# size :integer not null -# created_at :datetime -# updated_at :datetime -# file :string -# - class LfsObject < ActiveRecord::Base has_many :lfs_objects_projects, dependent: :destroy has_many :projects, through: :lfs_objects_projects diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb index 890736bfc80..0fd5f089db9 100644 --- a/app/models/lfs_objects_project.rb +++ b/app/models/lfs_objects_project.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: lfs_objects_projects -# -# id :integer not null, primary key -# lfs_object_id :integer not null -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# - class LfsObjectsProject < ActiveRecord::Base belongs_to :project belongs_to :lfs_object diff --git a/app/models/member.rb b/app/models/member.rb index 9a56d3d2181..b44b6c55ad8 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -1,22 +1,3 @@ -# == Schema Information -# -# Table name: members -# -# id :integer not null, primary key -# access_level :integer not null -# source_id :integer not null -# source_type :string not null -# user_id :integer -# notification_level :integer not null -# type :string -# created_at :datetime -# updated_at :datetime -# created_by_id :integer -# invite_email :string -# invite_token :string -# invite_accepted_at :datetime -# - class Member < ActiveRecord::Base include Sortable include Importable diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index a48c1943e6f..f63a0debf1a 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -1,22 +1,3 @@ -# == Schema Information -# -# Table name: members -# -# id :integer not null, primary key -# access_level :integer not null -# source_id :integer not null -# source_type :string not null -# user_id :integer -# notification_level :integer not null -# type :string -# created_at :datetime -# updated_at :datetime -# created_by_id :integer -# invite_email :string -# invite_token :string -# invite_accepted_at :datetime -# - class GroupMember < Member SOURCE_TYPE = 'Namespace' diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 143350a0b55..46955b430f3 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -1,22 +1,3 @@ -# == Schema Information -# -# Table name: members -# -# id :integer not null, primary key -# access_level :integer not null -# source_id :integer not null -# source_type :string not null -# user_id :integer -# notification_level :integer not null -# type :string -# created_at :datetime -# updated_at :datetime -# created_by_id :integer -# invite_email :string -# invite_token :string -# invite_accepted_at :datetime -# - class ProjectMember < Member SOURCE_TYPE = 'Project' @@ -24,7 +5,6 @@ class ProjectMember < Member belongs_to :project, class_name: 'Project', foreign_key: 'source_id' - # Make sure project member points only to project as it source default_value_for :source_type, SOURCE_TYPE validates_format_of :source_type, with: /\AProject\z/ @@ -34,6 +14,8 @@ class ProjectMember < Member scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) } scope :with_user, ->(user) { where(user_id: user.id) } + before_destroy :delete_member_todos + class << self # Add users to project teams with passed access option @@ -121,6 +103,10 @@ class ProjectMember < Member private + def delete_member_todos + user.todos.where(project_id: source_id).destroy_all if user + end + def send_invite notification_service.invite_project_member(self, @raw_invite_token) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c771968627c..73bf182ec9f 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1,33 +1,3 @@ -# == Schema Information -# -# Table name: merge_requests -# -# id :integer not null, primary key -# target_branch :string not null -# source_branch :string not null -# source_project_id :integer not null -# author_id :integer -# assignee_id :integer -# title :string -# created_at :datetime -# updated_at :datetime -# milestone_id :integer -# state :string -# merge_status :string -# target_project_id :integer not null -# iid :integer -# description :text -# position :integer default(0) -# locked_at :datetime -# updated_by_id :integer -# merge_error :string -# merge_params :text -# merge_when_build_succeeds :boolean default(FALSE), not null -# merge_user_id :integer -# merge_commit_sha :string -# deleted_at :datetime -# - class MergeRequest < ActiveRecord::Base include InternalId include Issuable @@ -57,6 +27,10 @@ class MergeRequest < ActiveRecord::Base # when creating new merge request attr_accessor :can_be_created, :compare_commits, :compare + # Temporary fields to store target_sha, and base_sha to + # compare when importing pull requests from GitHub + attr_accessor :base_target_sha, :head_source_sha + state_machine :state, initial: :opened do event :close do transition [:reopened, :opened] => :closed @@ -287,19 +261,20 @@ class MergeRequest < ActiveRecord::Base end def mergeable? - return false unless open? && !work_in_progress? && !broken? + return false unless mergeable_state? check_if_can_be_merged can_be_merged? end - def gitlab_merge_status - if work_in_progress? - "work_in_progress" - else - merge_status_name - end + def mergeable_state? + return false unless open? + return false if work_in_progress? + return false if broken? + return false unless mergeable_ci_state? + + true end def can_cancel_merge_when_build_succeeds?(current_user) @@ -313,6 +288,18 @@ class MergeRequest < ActiveRecord::Base last_commit == source_project.commit(source_branch) end + def should_remove_source_branch? + merge_params['should_remove_source_branch'].present? + end + + def force_remove_source_branch? + merge_params['force_remove_source_branch'].present? + end + + def remove_source_branch? + should_remove_source_branch? || force_remove_source_branch? + end + def mr_and_commit_notes # Fetch comments only from last 100 commits commits_for_notes_limit = 100 @@ -328,13 +315,6 @@ class MergeRequest < ActiveRecord::Base ) end - # Returns the raw diff for this merge request - # - # see "git diff" - def to_diff - target_project.repository.diff_text(diff_base_commit.sha, source_sha) - end - # Returns the commit as a series of email patches. # # see "git format-patch" @@ -453,7 +433,10 @@ class MergeRequest < ActiveRecord::Base self.merge_when_build_succeeds = false self.merge_user = nil - self.merge_params = nil + if merge_params + merge_params.delete('should_remove_source_branch') + merge_params.delete('commit_message') + end self.save end @@ -500,6 +483,12 @@ class MergeRequest < ActiveRecord::Base ::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch) end + def mergeable_ci_state? + return true unless project.only_allow_merge_if_build_succeeds? + + !pipeline || pipeline.success? + end + def state_human_name if merged? "Merged" @@ -521,10 +510,14 @@ class MergeRequest < ActiveRecord::Base end def target_sha - @target_sha ||= target_project.repository.commit(target_branch).try(:sha) + return @base_target_sha if defined?(@base_target_sha) + + target_project.repository.commit(target_branch).try(:sha) end def source_sha + return @head_source_sha if defined?(@head_source_sha) + last_commit.try(:sha) || source_tip.try(:sha) end @@ -545,7 +538,7 @@ class MergeRequest < ActiveRecord::Base end def ref_is_fetched? - File.exists?(File.join(project.repository.path_to_repo, ref_path)) + File.exist?(File.join(project.repository.path_to_repo, ref_path)) end def ensure_ref_fetched @@ -587,8 +580,8 @@ class MergeRequest < ActiveRecord::Base diverged_commits_count > 0 end - def ci_commit - @ci_commit ||= source_project.ci_commit(last_commit.id, source_branch) if last_commit && source_project + def pipeline + @pipeline ||= source_project.pipeline(last_commit.id, source_branch) if last_commit && source_project end def diff_refs diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 2c9eaf3849f..aca377cc600 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: merge_request_diffs -# -# id :integer not null, primary key -# state :string -# st_commits :text -# st_diffs :text -# merge_request_id :integer not null -# created_at :datetime -# updated_at :datetime -# base_commit_sha :string -# real_size :string -# - class MergeRequestDiff < ActiveRecord::Base include Sortable include Importable @@ -22,7 +7,7 @@ class MergeRequestDiff < ActiveRecord::Base belongs_to :merge_request - delegate :target_branch, :source_branch, to: :merge_request, prefix: nil + delegate :head_source_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil state_machine :state, initial: :empty do state :collected @@ -54,8 +39,8 @@ class MergeRequestDiff < ActiveRecord::Base @diffs_no_whitespace ||= begin compare = Gitlab::Git::Compare.new( self.repository.raw_repository, - self.target_branch, - self.source_sha, + self.base, + self.head, ) compare.diffs(options) end @@ -114,9 +99,7 @@ class MergeRequestDiff < ActiveRecord::Base commits = compare.commits if commits.present? - commits = Commit.decorate(commits, merge_request.source_project). - sort_by(&:created_at). - reverse + commits = Commit.decorate(commits, merge_request.source_project).reverse end commits @@ -160,7 +143,7 @@ class MergeRequestDiff < ActiveRecord::Base self.st_diffs = new_diffs - self.base_commit_sha = self.repository.merge_base(self.source_sha, self.target_branch) + self.base_commit_sha = self.repository.merge_base(self.head, self.base) self.save end @@ -176,10 +159,24 @@ class MergeRequestDiff < ActiveRecord::Base end def source_sha + return head_source_sha if head_source_sha.present? + source_commit = merge_request.source_project.commit(source_branch) source_commit.try(:sha) end + def target_sha + merge_request.target_sha + end + + def base + self.target_sha || self.target_branch + end + + def head + self.source_sha + end + def compare @compare ||= begin @@ -188,8 +185,8 @@ class MergeRequestDiff < ActiveRecord::Base Gitlab::Git::Compare.new( self.repository.raw_repository, - self.target_branch, - self.source_sha + self.base, + self.head ) end end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 5ee8a965ad8..e0c8454a998 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: milestones -# -# id :integer not null, primary key -# title :string not null -# project_id :integer not null -# description :text -# due_date :date -# created_at :datetime -# updated_at :datetime -# state :string -# iid :integer -# - class Milestone < ActiveRecord::Base # Represents a "No Milestone" state used for filtering Issues and Merge # Requests that have no milestone assigned. @@ -74,25 +59,67 @@ class Milestone < ActiveRecord::Base end end + def self.reference_prefix + '%' + end + def self.reference_pattern - nil + # NOTE: The iid pattern only matches when all characters on the expression + # are digits, so it will match %2 but not %2.1 because that's probably a + # milestone name and we want it to be matched as such. + @reference_pattern ||= %r{ + (#{Project.reference_pattern})? + #{Regexp.escape(reference_prefix)} + (?: + (?<milestone_iid> + \d+(?!\S\w)\b # Integer-based milestone iid, or + ) | + (?<milestone_name> + [^"\s]+\b | # String-based single-word milestone title, or + "[^"]+" # String-based multi-word milestone surrounded in quotes + ) + ) + }x end def self.link_reference_pattern @link_reference_pattern ||= super("milestones", /(?<milestone>\d+)/) end - def self.upcoming - self.where('due_date > ?', Time.now).reorder(due_date: :asc).first - end + def self.upcoming_ids_by_projects(projects) + rel = unscoped.of_projects(projects).active.where('due_date > ?', Time.now) - def to_reference(from_project = nil) - escaped_title = self.title.gsub("]", "\\]") + if Gitlab::Database.postgresql? + rel.order(:project_id, :due_date).select('DISTINCT ON (project_id) id') + else + rel. + group(:project_id). + having('due_date = MIN(due_date)'). + pluck(:id, :project_id, :due_date). + map(&:first) + end + end - h = Gitlab::Routing.url_helpers - url = h.namespace_project_milestone_url(self.project.namespace, self.project, self) + ## + # Returns the String necessary to reference this Milestone in Markdown + # + # format - Symbol format to use (default: :iid, optional: :name) + # + # Examples: + # + # Milestone.first.to_reference # => "%1" + # Milestone.first.to_reference(format: :name) # => "%\"goal\"" + # Milestone.first.to_reference(project) # => "gitlab-org/gitlab-ce%1" + # + def to_reference(from_project = nil, format: :iid) + format_reference = milestone_format_reference(format) + reference = "#{self.class.reference_prefix}#{format_reference}" - "[#{escaped_title}](#{url})" + if cross_project_reference?(from_project) + project.to_reference + reference + else + reference + end end def reference_link_text(from_project = nil) @@ -129,6 +156,10 @@ class Milestone < ActiveRecord::Base nil end + def title=(value) + write_attribute(:title, Sanitize.clean(value.to_s)) if value.present? + end + # Sorts the issues for the given IDs. # # This method runs a single SQL query using a CASE statement to update the @@ -160,4 +191,16 @@ class Milestone < ActiveRecord::Base issues.where(id: ids). update_all(["position = CASE #{conditions} ELSE position END", *pairs]) end + + private + + def milestone_format_reference(format = :iid) + raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format) + + if format == :name && !name.include?('"') + %("#{name}") + else + iid + end + end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 741e912171d..da19462f265 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -1,20 +1,3 @@ -# == Schema Information -# -# Table name: namespaces -# -# id :integer not null, primary key -# name :string not null -# path :string not null -# owner_id :integer -# created_at :datetime -# updated_at :datetime -# type :string -# description :string default(""), not null -# avatar :string -# share_with_group_lock :boolean default(FALSE) -# visibility_level :integer default(20), not null -# - class Namespace < ActiveRecord::Base include Sortable include Gitlab::ShellAdapter @@ -127,6 +110,10 @@ class Namespace < ActiveRecord::Base # Ensure old directory exists before moving it gitlab_shell.add_namespace(path_was) + if any_project_has_container_registry_tags? + raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry') + end + if gitlab_shell.mv_namespace(path_was, path) Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) @@ -148,6 +135,10 @@ class Namespace < ActiveRecord::Base end end + def any_project_has_container_registry_tags? + projects.any?(&:has_container_registry_tags?) + end + def send_update_instructions projects.each do |project| project.send_move_instructions("#{path_was}/#{project.path}") diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index f4e90125373..a2aee2f925b 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -22,9 +22,16 @@ module Network def collect_notes h = Hash.new(0) - @project.notes.where('noteable_type = ?' ,"Commit").group('notes.commit_id').select('notes.commit_id, count(notes.id) as note_count').each do |item| - h[item.commit_id] = item.note_count.to_i - end + + @project + .notes + .where('noteable_type = ?', 'Commit') + .group('notes.commit_id') + .select('notes.commit_id, count(notes.id) as note_count') + .each do |item| + h[item.commit_id] = item.note_count.to_i + end + h end @@ -89,7 +96,7 @@ module Network end end - if self.class.max_count / 2 < offset then + if self.class.max_count / 2 < offset # get max index that commit is displayed in the center. offset - self.class.max_count / 2 else @@ -130,7 +137,7 @@ module Network commit.parents(@map).each do |parent| range = commit.time..parent.time - space = if commit.space >= parent.space then + space = if commit.space >= parent.space find_free_parent_space(range, parent.space, -1, commit.space) else find_free_parent_space(range, commit.space, -1, parent.space) @@ -144,7 +151,7 @@ module Network end def find_free_parent_space(range, space_base, space_step, space_default) - if is_overlap?(range, space_default) then + if is_overlap?(range, space_default) find_free_space(range, space_step, space_base, space_default) else space_default @@ -155,9 +162,9 @@ module Network range.each do |i| if i != range.first && i != range.last && - @commits[i].spaces.include?(overlap_space) then + @commits[i].spaces.include?(overlap_space) - return true; + return true end end @@ -198,7 +205,7 @@ module Network # Visit branching chains leaves.each do |l| parents = l.parents(@map).select{|p| p.space.zero?} - for p in parents + parents.each do |p| place_chain(p, l.time) end end @@ -216,7 +223,7 @@ module Network end def mark_reserved(time_range, space) - for day in time_range + time_range.each do |day| @reserved[day].push(space) end end @@ -225,15 +232,15 @@ module Network space_default ||= space_base reserved = [] - for day in time_range + time_range.each do |day| reserved.push(*@reserved[day]) end reserved.uniq! space = space_default - while reserved.include?(space) do + while reserved.include?(space) space += space_step - if space < space_base then + if space < space_base space_step *= -1 space = space_base + space_step end @@ -253,7 +260,7 @@ module Network leaves = [] leaves.push(commit) if commit.space.zero? - while true + loop do return leaves if commit.parents(@map).count.zero? commit = commit.parents(@map).first diff --git a/app/models/note.rb b/app/models/note.rb index deee2b9e885..585d8c4ad84 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -1,34 +1,13 @@ -# == Schema Information -# -# Table name: notes -# -# id :integer not null, primary key -# note :text -# noteable_type :string -# author_id :integer -# created_at :datetime -# updated_at :datetime -# project_id :integer -# attachment :string -# line_code :string -# commit_id :string -# noteable_id :integer -# system :boolean default(FALSE), not null -# st_diff :text -# updated_by_id :integer -# is_award :boolean default(FALSE), not null -# - -require 'carrierwave/orm/activerecord' - class Note < ActiveRecord::Base + extend ActiveModel::Naming include Gitlab::CurrentSettings include Participable include Mentionable + include Awardable default_value_for :system, false - attr_mentionable :note, cache: true, pipeline: :note + attr_mentionable :note, pipeline: :note participant :author belongs_to :project @@ -41,29 +20,28 @@ class Note < ActiveRecord::Base delegate :gfm_reference, :local_reference, to: :noteable delegate :name, to: :project, prefix: true delegate :name, :email, to: :author, prefix: true - - before_validation :set_award! - before_validation :clear_blank_line_code! + delegate :title, to: :noteable, allow_nil: true validates :note, :project, presence: true - validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } - validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award } - validates :line_code, line_code: true, allow_blank: true + # Attachments are deprecated and are handled by Markdown uploader validates :attachment, file_size: { maximum: :max_attachment_size } - validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' } - validates :commit_id, presence: true, if: ->(n) { n.noteable_type == 'Commit' } + validates :noteable_type, presence: true + validates :noteable_id, presence: true, unless: :for_commit? + validates :commit_id, presence: true, if: :for_commit? validates :author, presence: true + validate unless: :for_commit? do |note| + unless note.noteable.try(:project) == note.project + errors.add(:invalid_project, 'Note and noteable project mismatch') + end + end + mount_uploader :attachment, AttachmentUploader # Scopes - scope :awards, ->{ where(is_award: true) } - scope :nonawards, ->{ where(is_award: false) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } - scope :inline, ->{ where("line_code IS NOT NULL") } - scope :not_inline, ->{ where(line_code: nil) } scope :system, ->{ where(system: true) } scope :user, ->{ where(system: false) } scope :common, ->{ where(noteable_type: ["", nil]) } @@ -71,65 +49,61 @@ class Note < ActiveRecord::Base scope :inc_author_project, ->{ includes(:project, :author) } scope :inc_author, ->{ includes(:author) } + scope :legacy_diff_notes, ->{ where(type: 'LegacyDiffNote') } + scope :non_diff_notes, ->{ where(type: ['Note', nil]) } + scope :with_associations, -> do includes(:author, :noteable, :updated_by, project: [:project_members, { group: [:group_members] }]) end - serialize :st_diff - before_create :set_diff, if: ->(n) { n.line_code.present? } + before_validation :clear_blank_line_code! class << self - def discussions_from_notes(notes) - discussion_ids = [] - discussions = [] - - notes.each do |note| - next if discussion_ids.include?(note.discussion_id) - - # don't group notes for the main target - if !note.for_diff_line? && note.for_merge_request? - discussions << [note] - else - discussions << notes.select do |other_note| - note.discussion_id == other_note.discussion_id - end - discussion_ids << note.discussion_id - end - end + def model_name + ActiveModel::Name.new(self, nil, 'note') + end + + def build_discussion_id(noteable_type, noteable_id) + [:discussion, noteable_type.try(:underscore), noteable_id].join("-") + end - discussions + def discussions + all.group_by(&:discussion_id).values end - def build_discussion_id(type, id, line_code) - [:discussion, type.try(:underscore), id, line_code].join("-").to_sym + def grouped_diff_notes + legacy_diff_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) end # Searches for notes matching the given query. # # This method uses ILIKE on PostgreSQL and LIKE on MySQL. # - # query - The search query as a String. + # query - The search query as a String. + # as_user - Limit results to those viewable by a specific user # # Returns an ActiveRecord::Relation. - def search(query) + def search(query, as_user: nil) table = arel_table pattern = "%#{query}%" - where(table[:note].matches(pattern)) - end - - def grouped_awards - notes = {} - - awards.select(:note).distinct.map do |note| - notes[note.note] = where(note: note.note) + found_notes = joins('LEFT JOIN issues ON issues.id = noteable_id'). + where(table[:note].matches(pattern)) + + if as_user + found_notes.where(' + issues.confidential IS NULL + OR issues.confidential IS FALSE + OR (issues.confidential IS TRUE + AND (issues.author_id = :user_id + OR issues.assignee_id = :user_id + OR issues.project_id IN(:project_ids)))', + user_id: as_user.id, + project_ids: as_user.authorized_projects.select(:id)) + else + found_notes.where('issues.confidential IS NULL OR issues.confidential IS FALSE') end - - notes["thumbsup"] ||= Note.none - notes["thumbsdown"] ||= Note.none - - notes end end @@ -137,167 +111,39 @@ class Note < ActiveRecord::Base system && SystemNoteService.cross_reference?(note) end - def max_attachment_size - current_application_settings.max_attachment_size.megabytes.to_i + def diff_note? + false end - def find_diff - return nil unless noteable - return @diff if defined?(@diff) - - # Don't use ||= because nil is a valid value for @diff - @diff = noteable.diffs(Commit.max_diff_options).find do |d| - Digest::SHA1.hexdigest(d.new_path) == diff_file_index if d.new_path - end + def legacy_diff_note? + false end - def hook_attrs - attributes - end - - def set_diff - # First lets find notes with same diff - # before iterating over all mr diffs - diff = diff_for_line_code unless for_merge_request? - diff ||= find_diff - - self.st_diff = diff.to_hash if diff - end - - def diff - @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) - end - - def diff_for_line_code - Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff) - end - - # Check if this note is part of an "active" discussion - # - # This will always return true for anything except MergeRequest noteables, - # which have special logic. - # - # If the note's current diff cannot be matched in the MergeRequest's current - # diff, it's considered inactive. def active? - return true unless self.diff - return false unless noteable - return @active if defined?(@active) - - noteable_diff = find_noteable_diff - - if noteable_diff - parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line) - - @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line } - else - @active = false - end - - @active - end - - def diff_file_index - line_code.split('_')[0] if line_code - end - - def diff_file_name - diff.new_path if diff - end - - def file_path - if diff.new_path.present? - diff.new_path - elsif diff.old_path.present? - diff.old_path - end + true end - def diff_old_line - line_code.split('_')[1].to_i if line_code - end - - def diff_new_line - line_code.split('_')[2].to_i if line_code - end - - def generate_line_code(line) - Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) - end - - def diff_line - return @diff_line if @diff_line - - if diff - diff_lines.each do |line| - if generate_line_code(line) == self.line_code - @diff_line = line.text - end - end - end - - @diff_line - end - - def diff_line_type - return @diff_line_type if @diff_line_type - - if diff - diff_lines.each do |line| - if generate_line_code(line) == self.line_code - @diff_line_type = line.type - end - end - end - - @diff_line_type - end - - def truncated_diff_lines - max_number_of_lines = 16 - prev_match_line = nil - prev_lines = [] - - highlighted_diff_lines.each do |line| - if line.type == "match" - prev_lines.clear - prev_match_line = line + def discussion_id + @discussion_id ||= + if for_merge_request? + [:discussion, :note, id].join("-") else - prev_lines << line - - break if generate_line_code(line) == self.line_code - - prev_lines.shift if prev_lines.length >= max_number_of_lines + self.class.build_discussion_id(noteable_type, noteable_id || commit_id) end - end - - prev_lines end - def diff_lines - @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line) - end - - def highlighted_diff_lines - Gitlab::Diff::Highlight.new(diff_lines).highlight + def max_attachment_size + current_application_settings.max_attachment_size.megabytes.to_i end - def discussion_id - @discussion_id ||= Note.build_discussion_id(noteable_type, noteable_id || commit_id, line_code) + def hook_attrs + attributes end def for_commit? noteable_type == "Commit" end - def for_commit_diff_line? - for_commit? && for_diff_line? - end - - def for_diff_line? - line_code.present? - end - def for_issue? noteable_type == "Issue" end @@ -306,10 +152,6 @@ class Note < ActiveRecord::Base noteable_type == "MergeRequest" end - def for_merge_request_diff_line? - for_merge_request? && for_diff_line? - end - def for_snippet? noteable_type == "Snippet" end @@ -346,50 +188,24 @@ class Note < ActiveRecord::Base Event.reset_event_cache_for(self) end - def downvote? - is_award && note == "thumbsdown" - end - - def upvote? - is_award && note == "thumbsup" - end - def editable? - !system? && !is_award + !system? end def cross_reference_not_visible_for?(user) cross_reference? && referenced_mentionables(user).empty? end - # Checks if note is an award added as a comment - # - # If note is an award, this method sets is_award to true - # and changes content of the note to award name. - # - # Method is executed as a before_validation callback. - # - def set_award! - return unless awards_supported? && contains_emoji_only? - - self.is_award = true - self.note = award_emoji_name + def award_emoji? + award_emoji_supported? && contains_emoji_only? end - private - def clear_blank_line_code! self.line_code = nil if self.line_code.blank? end - # Find the diff on noteable that matches our own - def find_noteable_diff - diffs = noteable.diffs(Commit.max_diff_options) - diffs.find { |d| d.new_path == self.diff.new_path } - end - - def awards_supported? - (for_issue? || for_merge_request?) && !for_diff_line? + def award_emoji_supported? + noteable.is_a?(Awardable) end def contains_emoji_only? @@ -398,6 +214,6 @@ class Note < ActiveRecord::Base def award_emoji_name original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] - AwardEmoji.normilize_emoji_name(original_name) + Gitlab::AwardEmoji.normalize_emoji_name(original_name) end end diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 846773752a6..17fb15b08df 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -1,18 +1,5 @@ -# == Schema Information -# -# Table name: notification_settings -# -# id :integer not null, primary key -# user_id :integer not null -# source_id :integer not null -# source_type :string not null -# level :integer default(0), not null -# created_at :datetime not null -# updated_at :datetime not null -# - class NotificationSetting < ActiveRecord::Base - enum level: { disabled: 0, participating: 1, watch: 2, global: 3, mention: 4 } + enum level: { global: 3, watch: 2, mention: 4, participating: 1, disabled: 0 } default_value_for :level, NotificationSetting.levels[:global] diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb index c78c7f4aa0e..116fb71ac08 100644 --- a/app/models/oauth_access_token.rb +++ b/app/models/oauth_access_token.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: oauth_access_tokens -# -# id :integer not null, primary key -# resource_owner_id :integer -# application_id :integer -# token :string not null -# refresh_token :string -# expires_in :integer -# revoked_at :datetime -# created_at :datetime not null -# scopes :string -# - class OauthAccessToken < ActiveRecord::Base belongs_to :resource_owner, class_name: 'User' belongs_to :application, class_name: 'Doorkeeper::Application' diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb index 1d5f4c50254..82c1c4de3a0 100644 --- a/app/models/personal_snippet.rb +++ b/app/models/personal_snippet.rb @@ -1,18 +1,2 @@ -# == Schema Information -# -# Table name: snippets -# -# id :integer not null, primary key -# title :string -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string -# type :string -# visibility_level :integer default(0), not null -# - class PersonalSnippet < Snippet end diff --git a/app/models/project.rb b/app/models/project.rb index 5003324f8f8..ab7947a0880 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1,48 +1,3 @@ -# == Schema Information -# -# Table name: projects -# -# id :integer not null, primary key -# name :string -# path :string -# description :text -# created_at :datetime -# updated_at :datetime -# creator_id :integer -# issues_enabled :boolean default(TRUE), not null -# merge_requests_enabled :boolean default(TRUE), not null -# wiki_enabled :boolean default(TRUE), not null -# namespace_id :integer -# issues_tracker :string default("gitlab"), not null -# issues_tracker_id :string -# snippets_enabled :boolean default(TRUE), not null -# last_activity_at :datetime -# import_url :string -# visibility_level :integer default(0), not null -# archived :boolean default(FALSE), not null -# avatar :string -# import_status :string -# repository_size :float default(0.0) -# star_count :integer default(0), not null -# import_type :string -# import_source :string -# commit_count :integer default(0) -# import_error :text -# ci_id :integer -# builds_enabled :boolean default(TRUE), not null -# shared_runners_enabled :boolean default(TRUE), not null -# runners_token :string -# build_coverage_regex :string -# build_allow_git_fetch :boolean default(TRUE), not null -# build_timeout :integer default(3600), not null -# pending_delete :boolean default(FALSE) -# public_builds :boolean default(TRUE), not null -# main_language :string -# pushes_since_gc :integer default(0) -# last_repository_check_failed :boolean -# last_repository_check_at :datetime -# - require 'carrierwave/orm/activerecord' class Project < ActiveRecord::Base @@ -67,6 +22,7 @@ class Project < ActiveRecord::Base default_value_for :builds_enabled, gitlab_config_features.builds default_value_for :wiki_enabled, gitlab_config_features.wiki default_value_for :snippets_enabled, gitlab_config_features.snippets + default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } # set last_activity_at to the same as created_at @@ -94,6 +50,8 @@ class Project < ActiveRecord::Base attr_accessor :new_default_branch attr_accessor :old_path_with_namespace + alias_attribute :title, :name + # Relations belongs_to :creator, foreign_key: 'creator_id', class_name: 'User' belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id' @@ -161,7 +119,7 @@ class Project < ActiveRecord::Base has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_many :commit_statuses, dependent: :destroy, class_name: 'CommitStatus', foreign_key: :gl_project_id - has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id + has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' @@ -213,17 +171,17 @@ class Project < ActiveRecord::Base scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } - scope :sorted_by_names, -> { joins(:namespace).reorder('namespaces.name ASC, projects.name ASC') } - scope :without_user, ->(user) { where('projects.id NOT IN (:ids)', ids: user.authorized_projects.map(&:id) ) } - scope :without_team, ->(team) { team.projects.present? ? where('projects.id NOT IN (:ids)', ids: team.projects.map(&:id)) : scoped } - scope :not_in_group, ->(group) { where('projects.id NOT IN (:ids)', ids: group.project_ids ) } scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) } - scope :in_group_namespace, -> { joins(:group) } scope :personal, ->(user) { where(namespace_id: user.namespace_id) } scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) } + scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) } scope :non_archived, -> { where(archived: false) } scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } + scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) } + + scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') } + scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) } state_machine :import_status, initial: :none do event :import_start do @@ -246,23 +204,10 @@ class Project < ActiveRecord::Base state :finished state :failed - after_transition any => :started, do: :schedule_add_import_job - after_transition any => :finished, do: :clear_import_data + after_transition any => :finished, do: :reset_cache_and_import_attrs end class << self - def abandoned - where('projects.last_activity_at < ?', 6.months.ago) - end - - def with_push - joins(:events).where('events.action = ?', Event::PUSHED) - end - - def active - joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') - end - # Searches for a list of projects based on the query given in `query`. # # On PostgreSQL this method uses "ILIKE" to perform a case-insensitive @@ -308,24 +253,69 @@ class Project < ActiveRecord::Base non_archived.where(table[:name].matches(pattern)) end - def find_with_namespace(id) - namespace_path, project_path = id.split('/', 2) - - return nil if !namespace_path || !project_path + # Finds a single project for the given path. + # + # path - The full project path (including namespace path). + # + # Returns a Project, or nil if no project could be found. + def find_with_namespace(path) + where_paths_in([path]).reorder(nil).take + end - # Use of unscoped ensures we're not secretly adding any ORDER BYs, which - # have a negative impact on performance (and aren't needed for this - # query). - projects = unscoped. - joins(:namespace). - iwhere('namespaces.path' => namespace_path) + # Builds a relation to find multiple projects by their full paths. + # + # Each path must be in the following format: + # + # namespace_path/project_path + # + # For example: + # + # gitlab-org/gitlab-ce + # + # Usage: + # + # Project.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee}) + # + # This would return the projects with the full paths matching the values + # given. + # + # paths - An Array of full paths (namespace path + project path) for which + # to find the projects. + # + # Returns an ActiveRecord::Relation. + def where_paths_in(paths) + wheres = [] + cast_lower = Gitlab::Database.postgresql? + + paths.each do |path| + namespace_path, project_path = path.split('/', 2) + + next unless namespace_path && project_path + + namespace_path = connection.quote(namespace_path) + project_path = connection.quote(project_path) + + where = "(namespaces.path = #{namespace_path} + AND projects.path = #{project_path})" + + if cast_lower + where = "( + #{where} + OR ( + LOWER(namespaces.path) = LOWER(#{namespace_path}) + AND LOWER(projects.path) = LOWER(#{project_path}) + ) + )" + end - projects.find_by('projects.path' => project_path) || - projects.iwhere('projects.path' => project_path).take - end + wheres << where + end - def find_by_ci_id(id) - find_by(ci_id: id.to_i) + if wheres.empty? + none + else + joins(:namespace).where(wheres.join(' OR ')) + end end def visibility_levels @@ -359,10 +349,6 @@ class Project < ActiveRecord::Base joins(join_body).reorder('join_note_counts.amount DESC') end - def visible_to_user(user) - where(id: user.authorized_projects.select(:id).reorder(nil)) - end - def create_from_import_job(current_user_id:, tmp_file:, namespace_id:, project_path:) job_id = ProjectImportWorker.perform_async(current_user_id, tmp_file, namespace_id, project_path) @@ -382,6 +368,34 @@ class Project < ActiveRecord::Base @repository ||= Repository.new(path_with_namespace, self) end + def container_registry_path_with_namespace + path_with_namespace.downcase + end + + def container_registry_repository + return unless Gitlab.config.registry.enabled + + @container_registry_repository ||= begin + token = Auth::ContainerRegistryAuthenticationService.full_access_token(container_registry_path_with_namespace) + url = Gitlab.config.registry.api_url + host_port = Gitlab.config.registry.host_port + registry = ContainerRegistry::Registry.new(url, token: token, path: host_port) + registry.repository(container_registry_path_with_namespace) + end + end + + def container_registry_repository_url + if Gitlab.config.registry.enabled + "#{Gitlab.config.registry.host_port}/#{container_registry_path_with_namespace}" + end + end + + def has_container_registry_tags? + return unless container_registry_repository + + container_registry_repository.tags.any? + end + def commit(id = 'HEAD') repository.commit(id) end @@ -395,10 +409,6 @@ class Project < ActiveRecord::Base id && persisted? end - def schedule_add_import_job - run_after_commit(:add_import_job) - end - def add_import_job if forked? job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) @@ -413,7 +423,7 @@ class Project < ActiveRecord::Base end end - def clear_import_data + def reset_cache_and_import_attrs update(import_error: nil) ProjectCacheWorker.perform_async(self.id) @@ -422,14 +432,14 @@ class Project < ActiveRecord::Base end def import_url=(value) - import_url = Gitlab::ImportUrl.new(value) + import_url = Gitlab::UrlSanitizer.new(value) create_or_update_import_data(credentials: import_url.credentials) super(import_url.sanitized_url) end def import_url if import_data && super - import_url = Gitlab::ImportUrl.new(super, credentials: import_data.credentials) + import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials) import_url.full_url else super @@ -479,17 +489,18 @@ class Project < ActiveRecord::Base end def safe_import_url - result = URI.parse(self.import_url) - result.password = '*****' unless result.password.nil? - result.user = '*****' unless result.user.nil? || result.user == "git" #tokens or other data may be saved as user - result.to_s - rescue - self.import_url + Gitlab::UrlSanitizer.new(import_url).masked_url end def check_limit unless creator.can_create_project? or namespace.kind == 'group' - self.errors.add(:limit_reached, "Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it") + projects_limit = creator.projects_limit + + if projects_limit == 0 + self.errors.add(:limit_reached, "Personal project creation is not allowed. Please contact your administrator with questions") + else + self.errors.add(:limit_reached, "Your project limit is #{projects_limit} projects! Please contact your administrator to increase it") + end end rescue self.errors.add(:base, "Can't check your ability to create project") @@ -571,9 +582,21 @@ class Project < ActiveRecord::Base end def external_issue_tracker - return @external_issue_tracker if defined?(@external_issue_tracker) - @external_issue_tracker ||= - services.issue_trackers.active.without_defaults.first + if has_external_issue_tracker.nil? # To populate existing projects + cache_has_external_issue_tracker + end + + if has_external_issue_tracker? + return @external_issue_tracker if defined?(@external_issue_tracker) + + @external_issue_tracker = services.external_issue_trackers.first + else + nil + end + end + + def cache_has_external_issue_tracker + update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) end def can_have_issues_tracker_id? @@ -797,6 +820,11 @@ class Project < ActiveRecord::Base expire_caches_before_rename(old_path_with_namespace) + if has_container_registry_tags? + # we currently doesn't support renaming repository if it contains tags in container registry + raise Exception.new('Project cannot be renamed, because tags are present in its container registry') + end + if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace) # If repository moved successfully we need to send update instructions to users. # However we cannot allow rollback since we moved repository @@ -973,12 +1001,12 @@ class Project < ActiveRecord::Base !namespace.share_with_group_lock end - def ci_commit(sha, ref) - ci_commits.order(id: :desc).find_by(sha: sha, ref: ref) + def pipeline(sha, ref) + pipelines.order(id: :desc).find_by(sha: sha, ref: ref) end - def ensure_ci_commit(sha, ref) - ci_commit(sha, ref) || ci_commits.create(sha: sha, ref: ref) + def ensure_pipeline(sha, ref) + pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref) end def enable_ci @@ -993,13 +1021,13 @@ class Project < ActiveRecord::Base shared_runners_enabled? && Ci::Runner.shared.active.any?(&block) end - def valid_runners_token? token + def valid_runners_token?(token) self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token) end # TODO (ayufan): For now we use runners_token (backward compatibility) # In 8.4 every build will have its own individual token valid for time of build - def valid_build_token? token + def valid_build_token?(token) self.builds_enabled? && self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token) end @@ -1055,6 +1083,24 @@ class Project < ActiveRecord::Base update_attribute(:pending_delete, true) end + def running_or_pending_build_count(force: false) + Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do + builds.running_or_pending.count(:all) + end + end + + def mark_import_as_failed(error_message) + original_errors = errors.dup + sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message) + + import_fail + update_column(:import_error, sanitized_message) + rescue ActiveRecord::ActiveRecordError => e + Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}") + ensure + @errors = original_errors + end + def add_export_job(current_user_id:) job_id = ProjectExportWorker.perform_async(current_user_id, self.id) diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index 66f5a609bf5..e52a6bd7c84 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: project_group_links -# -# id :integer not null, primary key -# project_id :integer not null -# group_id :integer not null -# created_at :datetime -# updated_at :datetime -# group_access :integer default(30), not null -# - class ProjectGroupLink < ActiveRecord::Base GUEST = 10 REPORTER = 20 diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index 7830f764ed3..ca8a9b4217b 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: project_import_data -# -# id :integer not null, primary key -# project_id :integer -# data :text -# encrypted_credentials :text -# encrypted_credentials_iv :string -# encrypted_credentials_salt :string -# - require 'carrierwave/orm/activerecord' class ProjectImportData < ActiveRecord::Base @@ -18,7 +6,8 @@ class ProjectImportData < ActiveRecord::Base key: Gitlab::Application.secrets.db_key_base, marshal: true, encode: true, - mode: :per_attribute_iv_and_salt + mode: :per_attribute_iv_and_salt, + algorithm: 'aes-256-cbc' serialize :data, JSON diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 368485a060a..7c23b766763 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - require 'asana' class AsanaService < Service diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index ffb7455b014..d839221d315 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class AssemblaService < Service include HTTParty diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index c36ee95e378..1d1780dcfbf 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class BambooService < CiService include HTTParty diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index f9f4897a065..86a06321e21 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - require "addressable/uri" class BuildkiteService < CiService diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 20cdfcaffb2..54da4d74fc5 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class BuildsEmailService < Service prop_accessor :recipients boolean_accessor :add_pusher diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 28c969fe57f..511b2eac792 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class CampfireService < Service prop_accessor :token, :subdomain, :room validates :token, presence: true, if: :activated? diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 9bc8f982da6..596c00705ad 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - # Base class for CI services # List methods you need to implement to get your CI service # working with GitLab Merge Requests diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index 4d1319eb6f8..6b2b1daa724 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class CustomIssueTrackerService < IssueTrackerService prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index d8e00e018cc..966dbc41d73 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class DroneCiService < CiService prop_accessor :drone_url, :token, :enable_ssl_verification diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 2dbd29062df..e0083c43adb 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class EmailsOnPushService < Service prop_accessor :send_from_committer_email prop_accessor :disable_diffs diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index 5469049bb5e..d7b6e505191 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class ExternalWikiService < Service include HTTParty @@ -49,7 +25,7 @@ class ExternalWikiService < Service def execute(_data) @response = HTTParty.get(properties['external_wiki_url'], verify: true) rescue nil - if @response !=200 + if @response != 200 nil end end diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 3dc1e0fbe8b..dd00275187f 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - require "flowdock-git-hook" class FlowdockService < Service diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index b4c311cf664..598aca5e06d 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - require "gemnasium/gitlab_service" class GemnasiumService < Service diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index a92f7226083..bbc312f5215 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - # TODO(ayufan): The GitLabCiService is deprecated and the type should be removed when the database entries are removed class GitlabCiService < CiService # We override the active accessor to always make GitLabCiService disabled diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 1adaeeb3b2b..5d17c358330 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class GitlabIssueTrackerService < IssueTrackerService include Gitlab::Routing.url_helpers diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index f9ddf588722..0ff4f4c8dd2 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class HipchatService < Service MAX_COMMITS = 3 diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index b9a592d7096..58cb720c3c1 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - require 'uri' class IrkerService < Service @@ -94,7 +70,7 @@ class IrkerService < Service private def get_channels - return true unless :activated? + return true unless activated? return true if recipients.nil? || recipients.empty? map_recipients @@ -107,7 +83,7 @@ class IrkerService < Service self.channels = recipients.split(/\s+/).map do |recipient| format_channel(recipient) end - channels.reject! &:nil? + channels.reject!(&:nil?) end def format_channel(recipient) diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 98a3a7c6b86..6ae9b16d3ce 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class IssueTrackerService < Service validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index ba68658f0bd..beda89d3963 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class JiraService < IssueTrackerService include HTTParty include Gitlab::Routing.url_helpers diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index acaa0c39365..ad19b7795da 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class PivotaltrackerService < Service include HTTParty diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index a640c8cb440..3dd878e4c7d 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class PushoverService < Service include HTTParty base_uri 'https://api.pushover.net/1' diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index e2137e92c62..11cce3e0561 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class RedmineService < IssueTrackerService prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 83ffa53a407..cf9e4d5a8b6 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class SlackService < Service prop_accessor :webhook, :username, :channel boolean_accessor :notify_only_broken_builds diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/slack_service/build_message.rb index c124cad4afd..69c21b3fc38 100644 --- a/app/models/project_services/slack_service/build_message.rb +++ b/app/models/project_services/slack_service/build_message.rb @@ -35,8 +35,8 @@ class SlackService private def message - "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} second(s)" - end + "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}" + end def format(string) Slack::Notifier::LinkFormatter.format(string) diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb index 438ff33fdff..88e053ec192 100644 --- a/app/models/project_services/slack_service/issue_message.rb +++ b/app/models/project_services/slack_service/issue_message.rb @@ -34,7 +34,12 @@ class SlackService private def message - "#{user_name} #{state} #{issue_link} in #{project_link}: *#{title}*" + case state + when "opened" + "[#{project_link}] Issue #{state} by #{user_name}" + else + "[#{project_link}] Issue #{issue_link} #{state} by #{user_name}" + end end def opened_issue? @@ -42,7 +47,11 @@ class SlackService end def description_message - [{ text: format(description), color: attachment_color }] + [{ + title: issue_title, + title_link: issue_url, + text: format(description), + color: "#C95823" }] end def project_link @@ -50,7 +59,11 @@ class SlackService end def issue_link - "[issue ##{issue_iid}](#{issue_url})" + "[#{issue_title}](#{issue_url})" + end + + def issue_title + "##{issue_iid} #{title}" end end end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 4015da31509..b0dcb52eba1 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - class TeamcityService < CiService include HTTParty diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index b4b2807eba4..25b5d777641 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -1,19 +1,3 @@ -# == Schema Information -# -# Table name: snippets -# -# id :integer not null, primary key -# title :string -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string -# type :string -# visibility_level :integer default(0), not null -# - class ProjectSnippet < Snippet belongs_to :project belongs_to :author, class_name: "User" @@ -23,5 +7,6 @@ class ProjectSnippet < Snippet # Scopes scope :fresh, -> { order("created_at DESC") } - participant :author, :notes + participant :author + participant :notes_with_associations end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 7c1a61bb0bf..25d82929c0b 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -27,6 +27,10 @@ class ProjectWiki @project.path_with_namespace + ".wiki" end + def web_url + Gitlab::Routing.url_helpers.namespace_project_wiki_url(@project.namespace, @project, :home) + end + def url_to_repo gitlab_shell.url_to_repo(path_with_namespace) end @@ -40,7 +44,7 @@ class ProjectWiki end def wiki_base_path - ["/", @project.path_with_namespace, "/wikis"].join('') + [Gitlab.config.gitlab.relative_url_root, "/", @project.path_with_namespace, "/wikis"].join('') end # Returns the Gollum::Wiki object. @@ -113,7 +117,7 @@ class ProjectWiki end def page_title_and_dir(title) - title_array = title.split("/") + title_array = title.split("/") title = title_array.pop [title, title_array.join("/")] end @@ -142,6 +146,16 @@ class ProjectWiki wiki end + def hook_attrs + { + web_url: web_url, + git_ssh_url: ssh_url_to_repo, + git_http_url: http_url_to_repo, + path_with_namespace: path_with_namespace, + default_branch: default_branch + } + end + private def init_repo(path_with_namespace) diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 3d2052c892c..33cf046fa75 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: protected_branches -# -# id :integer not null, primary key -# project_id :integer not null -# name :string not null -# created_at :datetime -# updated_at :datetime -# developers_can_push :boolean default(FALSE), not null -# - class ProtectedBranch < ActiveRecord::Base include Gitlab::ShellAdapter diff --git a/app/models/release.rb b/app/models/release.rb index dc700d1ea5a..e196b84eb18 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: releases -# -# id :integer not null, primary key -# tag :string -# description :text -# project_id :integer -# created_at :datetime -# updated_at :datetime -# - class Release < ActiveRecord::Base belongs_to :project diff --git a/app/models/repository.rb b/app/models/repository.rb index 7aebfe279fb..1ab163510bf 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -81,7 +81,7 @@ class Repository def commit(id = 'HEAD') return nil unless exists? commit = Gitlab::Git::Commit.find(raw_repository, id) - commit = Commit.new(commit, @project) if commit + commit = ::Commit.new(commit, @project) if commit commit rescue Rugged::OdbError nil @@ -195,6 +195,10 @@ class Repository cache.fetch(:branch_names) { branches.map(&:name) } end + def branch_exists?(branch_name) + branch_names.include?(branch_name) + end + def tag_names cache.fetch(:tag_names) { raw_repository.tag_names } end @@ -241,7 +245,7 @@ class Repository def cache_keys %i(size branch_names tag_names commit_count readme version contribution_guide changelog - license_blob license_key) + license_blob license_key gitignore) end def build_cache @@ -252,6 +256,10 @@ class Repository end end + def expire_gitignore + cache.expire(:gitignore) + end + def expire_tags_cache cache.expire(:tag_names) @tags = nil @@ -453,7 +461,7 @@ class Repository def version cache.fetch(:version) do tree(:head).blobs.find do |file| - file.name.downcase == 'version' + file.name.casecmp('version').zero? end end end @@ -468,33 +476,37 @@ class Repository def changelog cache.fetch(:changelog) do - tree(:head).blobs.find do |file| - file.name =~ /\A(changelog|history|changes|news)/i - end + file_on_head(/\A(changelog|history|changes|news)/i) end end def license_blob - return nil if !exists? || empty? + return nil unless head_exists? cache.fetch(:license_blob) do - tree(:head).blobs.find do |file| - file.name =~ /\A(licen[sc]e|copying)(\..+|\z)/i - end + file_on_head(/\A(licen[sc]e|copying)(\..+|\z)/i) end end def license_key - return nil if !exists? || empty? + return nil unless head_exists? cache.fetch(:license_key) do Licensee.license(path).try(:key) end end - def gitlab_ci_yml + def gitignore return nil if !exists? || empty? + cache.fetch(:gitignore) do + file_on_head(/\A\.gitignore\z/) + end + end + + def gitlab_ci_yml + return nil unless head_exists? + @gitlab_ci_yml ||= tree(:head).blobs.find do |file| file.name == '.gitlab-ci.yml' end @@ -795,7 +807,7 @@ class Repository def check_revert_content(commit, base_branch) source_sha = find_branch(base_branch).target args = [commit.id, source_sha] - args << { mainline: 1 } if commit.merge_commit? + args << { mainline: 1 } if commit.merge_commit? revert_index = rugged.revert_commit(*args) return false if revert_index.conflicts? @@ -809,7 +821,7 @@ class Repository def check_cherry_pick_content(commit, base_branch) source_sha = find_branch(base_branch).target args = [commit.id, source_sha] - args << 1 if commit.merge_commit? + args << 1 if commit.merge_commit? cherry_pick_index = rugged.cherrypick_commit(*args) return false if cherry_pick_index.conflicts? @@ -850,7 +862,7 @@ class Repository def search_files(query, ref) offset = 2 - args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -e #{Regexp.escape(query)} #{ref || root_ref}) + args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/) end @@ -960,12 +972,6 @@ class Repository end end - def main_language - return if empty? || rugged.head_unborn? - - Linguist::Repository.new(rugged, rugged.head.target_id).language - end - def avatar return nil unless exists? @@ -981,4 +987,12 @@ class Repository def cache @cache ||= RepositoryCache.new(path_with_namespace) end + + def head_exists? + exists? && !empty? && !rugged.head_unborn? + end + + def file_on_head(regex) + tree(:head).blobs.find { |file| file.name =~ regex } + end end diff --git a/app/models/security_event.rb b/app/models/security_event.rb index 0bee03974f1..d131c11cb6c 100644 --- a/app/models/security_event.rb +++ b/app/models/security_event.rb @@ -1,16 +1,2 @@ -# == Schema Information -# -# Table name: audit_events -# -# id :integer not null, primary key -# author_id :integer not null -# type :string not null -# entity_id :integer not null -# entity_type :string not null -# details :text -# created_at :datetime -# updated_at :datetime -# - class SecurityEvent < AuditEvent end diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index 99279a2e083..375f195dba7 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -1,17 +1,3 @@ -# == Schema Information -# -# Table name: sent_notifications -# -# id :integer not null, primary key -# project_id :integer -# noteable_id :integer -# noteable_type :string -# recipient_id :integer -# commit_id :string -# reply_key :string not null -# line_code :string -# - class SentNotification < ActiveRecord::Base belongs_to :project belongs_to :noteable, polymorphic: true diff --git a/app/models/service.rb b/app/models/service.rb index bf16a545307..bf352397509 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -1,27 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string -# title :string -# project_id :integer -# created_at :datetime not null -# updated_at :datetime not null -# active :boolean not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# category :string default("common"), not null -# default :boolean default(FALSE) -# wiki_page_events :boolean default(TRUE) -# - # To add new service you should build a class inherited from Service # and implement a set of methods class Service < ActiveRecord::Base @@ -40,6 +16,7 @@ class Service < ActiveRecord::Base after_initialize :initialize_properties after_commit :reset_updated_properties + after_commit :cache_project_has_external_issue_tracker belongs_to :project has_one :service_hook @@ -58,6 +35,7 @@ class Service < ActiveRecord::Base scope :note_hooks, -> { where(note_events: true, active: true) } scope :build_hooks, -> { where(build_events: true, active: true) } scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) } + scope :external_issue_trackers, -> { issue_trackers.active.without_defaults } default_value_for :category, 'common' @@ -216,4 +194,12 @@ class Service < ActiveRecord::Base service.project_id = project_id service if service.save end + + private + + def cache_project_has_external_issue_tracker + if project && !project.destroyed? + project.cache_has_external_issue_tracker + end + end end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 2f905a90942..f8034cb5e6b 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -1,19 +1,3 @@ -# == Schema Information -# -# Table name: snippets -# -# id :integer not null, primary key -# title :string -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string -# type :string -# visibility_level :integer default(0), not null -# - class Snippet < ActiveRecord::Base include Gitlab::VisibilityLevel include Linguist::BlobHelper @@ -46,7 +30,8 @@ class Snippet < ActiveRecord::Base scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } scope :fresh, -> { order("created_at DESC") } - participant :author, :notes + participant :author + participant :notes_with_associations def self.reference_prefix '$' @@ -116,6 +101,10 @@ class Snippet < ActiveRecord::Base content.lines.count > 1000 end + def notes_with_associations + notes.includes(:author) + end + class << self # Searches for snippets with a matching title or file name. # diff --git a/app/models/spam_log.rb b/app/models/spam_log.rb index f49eb7d88e2..12df68ef83b 100644 --- a/app/models/spam_log.rb +++ b/app/models/spam_log.rb @@ -1,20 +1,3 @@ -# == Schema Information -# -# Table name: spam_logs -# -# id :integer not null, primary key -# user_id :integer -# source_ip :string -# user_agent :string -# via_api :boolean -# project_id :integer -# noteable_type :string -# title :string -# description :text -# created_at :datetime not null -# updated_at :datetime not null -# - class SpamLog < ActiveRecord::Base belongs_to :user diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 242faa7d32e..3b8aa1eb866 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -1,16 +1,3 @@ -# == Schema Information -# -# Table name: subscriptions -# -# id :integer not null, primary key -# user_id :integer -# subscribable_id :integer -# subscribable_type :string -# subscribed :boolean -# created_at :datetime -# updated_at :datetime -# - class Subscription < ActiveRecord::Base belongs_to :user belongs_to :subscribable, polymorphic: true diff --git a/app/models/todo.rb b/app/models/todo.rb index d85f7bfdf57..3a091373329 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -1,24 +1,7 @@ -# == Schema Information -# -# Table name: todos -# -# id :integer not null, primary key -# user_id :integer not null -# project_id :integer not null -# target_id :integer -# target_type :string not null -# author_id :integer -# action :integer not null -# state :string not null -# created_at :datetime -# updated_at :datetime -# note_id :integer -# commit_id :string -# - class Todo < ActiveRecord::Base - ASSIGNED = 1 - MENTIONED = 2 + ASSIGNED = 1 + MENTIONED = 2 + BUILD_FAILED = 3 belongs_to :author, class_name: "User" belongs_to :note @@ -46,6 +29,10 @@ class Todo < ActiveRecord::Base state :done end + def build_failed? + action == BUILD_FAILED + end + def body if note.present? note.note diff --git a/app/models/u2f_registration.rb b/app/models/u2f_registration.rb new file mode 100644 index 00000000000..00b19686d48 --- /dev/null +++ b/app/models/u2f_registration.rb @@ -0,0 +1,40 @@ +# Registration information for U2F (universal 2nd factor) devices, like Yubikeys + +class U2fRegistration < ActiveRecord::Base + belongs_to :user + + def self.register(user, app_id, json_response, challenges) + u2f = U2F::U2F.new(app_id) + registration = self.new + + begin + response = U2F::RegisterResponse.load_from_json(json_response) + registration_data = u2f.register!(challenges, response) + registration.update(certificate: registration_data.certificate, + key_handle: registration_data.key_handle, + public_key: registration_data.public_key, + counter: registration_data.counter, + user: user) + rescue JSON::ParserError, NoMethodError, ArgumentError + registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.') + rescue U2F::Error => e + registration.errors.add(:base, e.message) + end + + registration + end + + def self.authenticate(user, app_id, json_response, challenges) + response = U2F::SignResponse.load_from_json(json_response) + registration = user.u2f_registrations.find_by_key_handle(response.key_handle) + u2f = U2F::U2F.new(app_id) + + if registration + u2f.authenticate!(challenges, response, Base64.decode64(registration.public_key), registration.counter) + registration.update(counter: response.counter) + true + end + rescue JSON::ParserError, NoMethodError, ArgumentError, U2F::Error + false + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 959b1f93758..e0987e07e1f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,68 +1,3 @@ -# == Schema Information -# -# Table name: users -# -# id :integer not null, primary key -# email :string default(""), not null -# encrypted_password :string default(""), not null -# reset_password_token :string -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string -# last_sign_in_ip :string -# created_at :datetime -# updated_at :datetime -# name :string -# admin :boolean default(FALSE), not null -# projects_limit :integer default(10) -# skype :string default(""), not null -# linkedin :string default(""), not null -# twitter :string default(""), not null -# authentication_token :string -# theme_id :integer default(1), not null -# bio :string -# failed_attempts :integer default(0) -# locked_at :datetime -# username :string -# can_create_group :boolean default(TRUE), not null -# can_create_team :boolean default(TRUE), not null -# state :string -# color_scheme_id :integer default(1), not null -# notification_level :integer default(1), not null -# password_expires_at :datetime -# created_by_id :integer -# last_credential_check_at :datetime -# avatar :string -# confirmation_token :string -# confirmed_at :datetime -# confirmation_sent_at :datetime -# unconfirmed_email :string -# hide_no_ssh_key :boolean default(FALSE) -# website_url :string default(""), not null -# notification_email :string -# hide_no_password :boolean default(FALSE) -# password_automatically_set :boolean default(FALSE) -# location :string -# encrypted_otp_secret :string -# encrypted_otp_secret_iv :string -# encrypted_otp_secret_salt :string -# otp_required_for_login :boolean default(FALSE), not null -# otp_backup_codes :text -# public_email :string default(""), not null -# dashboard :integer default(0) -# project_view :integer default(0) -# consumed_timestep :integer -# layout :integer default(0) -# hide_project_limit :boolean default(FALSE) -# unlock_token :string -# otp_grace_period_started_at :datetime -# ldap_email :boolean default(FALSE), not null -# external :boolean default(FALSE) -# - require 'carrierwave/orm/activerecord' class User < ActiveRecord::Base @@ -85,14 +20,18 @@ class User < ActiveRecord::Base default_value_for :hide_no_password, false default_value_for :theme_id, gitlab_config.default_theme + attr_encrypted :otp_secret, + key: Gitlab::Application.config.secret_key_base, + mode: :per_attribute_iv_and_salt, + algorithm: 'aes-256-cbc' + devise :two_factor_authenticatable, - otp_secret_encryption_key: File.read(Rails.root.join('.secret')).chomp - alias_attribute :two_factor_enabled, :otp_required_for_login + otp_secret_encryption_key: Gitlab::Application.config.secret_key_base devise :two_factor_backupable, otp_number_of_backup_codes: 10 serialize :otp_backup_codes, JSON - devise :lockable, :async, :recoverable, :rememberable, :trackable, + devise :lockable, :recoverable, :rememberable, :trackable, :validatable, :omniauthable, :confirmable, :registerable attr_accessor :force_random_password @@ -111,6 +50,7 @@ class User < ActiveRecord::Base has_many :keys, dependent: :destroy has_many :emails, dependent: :destroy has_many :identities, dependent: :destroy, autosave: true + has_many :u2f_registrations, dependent: :destroy # Groups has_many :members, dependent: :destroy @@ -144,6 +84,7 @@ class User < ActiveRecord::Base has_many :builds, dependent: :nullify, class_name: 'Ci::Build' has_many :todos, dependent: :destroy has_many :notification_settings, dependent: :destroy + has_many :award_emoji, as: :awardable, dependent: :destroy # # Validations @@ -177,6 +118,7 @@ class User < ActiveRecord::Base before_save :ensure_external_user_rights after_save :ensure_namespace_correct after_initialize :set_projects_limit + before_create :check_confirmation_email after_create :post_create_hook after_destroy :post_destroy_hook @@ -233,8 +175,16 @@ class User < ActiveRecord::Base scope :active, -> { with_state(:active) } scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') } - scope :with_two_factor, -> { where(two_factor_enabled: true) } - scope :without_two_factor, -> { where(two_factor_enabled: false) } + + def self.with_two_factor + joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id"). + where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id]) + end + + def self.without_two_factor + joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id"). + where("u2f.id IS NULL AND otp_required_for_login = ?", false) + end # # Class methods @@ -372,19 +322,38 @@ class User < ActiveRecord::Base @reset_token end + def check_confirmation_email + skip_confirmation! unless current_application_settings.send_user_confirmation_email + end + def recently_sent_password_reset? reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago end def disable_two_factor! - update_attributes( - two_factor_enabled: false, - encrypted_otp_secret: nil, - encrypted_otp_secret_iv: nil, - encrypted_otp_secret_salt: nil, - otp_grace_period_started_at: nil, - otp_backup_codes: nil - ) + transaction do + update_attributes( + otp_required_for_login: false, + encrypted_otp_secret: nil, + encrypted_otp_secret_iv: nil, + encrypted_otp_secret_salt: nil, + otp_grace_period_started_at: nil, + otp_backup_codes: nil + ) + self.u2f_registrations.destroy_all + end + end + + def two_factor_enabled? + two_factor_otp_enabled? || two_factor_u2f_enabled? + end + + def two_factor_otp_enabled? + self.otp_required_for_login? + end + + def two_factor_u2f_enabled? + self.u2f_registrations.exists? end def namespace_uniq @@ -446,17 +415,17 @@ class User < ActiveRecord::Base Project.where("projects.id IN (#{projects_union.to_sql})") end + def viewable_starred_projects + starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (#{projects_union.to_sql})", + [Project::PUBLIC, Project::INTERNAL]) + end + def owned_projects @owned_projects ||= Project.where('namespace_id IN (?) OR namespace_id = ?', owned_groups.select(:id), namespace.id).joins(:namespace) end - # Team membership in authorized projects - def tm_in_authorized_projects - ProjectMember.where(source_id: authorized_projects.map(&:id), user_id: self.id) - end - def is_admin? admin end @@ -546,10 +515,6 @@ class User < ActiveRecord::Base "#{name} (#{username})" end - def tm_of(project) - project.project_member_by_id(self.id) - end - def already_forked?(project) !!fork_of(project) end @@ -835,6 +800,23 @@ class User < ActiveRecord::Base notification_settings.find_or_initialize_by(source: source) end + def assigned_open_merge_request_count(force: false) + Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do + assigned_merge_requests.opened.count + end + end + + def assigned_open_issues_count(force: false) + Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do + assigned_issues.opened.count + end + end + + def update_cache_counts + assigned_open_merge_request_count(force: true) + assigned_open_issues_count(force: true) + end + private def projects_union diff --git a/app/models/users_star_project.rb b/app/models/users_star_project.rb index 413f3f485a8..0dfe597317e 100644 --- a/app/models/users_star_project.rb +++ b/app/models/users_star_project.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: users_star_projects -# -# id :integer not null, primary key -# project_id :integer not null -# user_id :integer not null -# created_at :datetime -# updated_at :datetime -# - class UsersStarProject < ActiveRecord::Base belongs_to :project, counter_cache: :star_count, touch: true belongs_to :user diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb new file mode 100644 index 00000000000..e57b95f21ec --- /dev/null +++ b/app/services/auth/container_registry_authentication_service.rb @@ -0,0 +1,87 @@ +module Auth + class ContainerRegistryAuthenticationService < BaseService + include Gitlab::CurrentSettings + + AUDIENCE = 'container_registry' + + def execute + return error('not found', 404) unless registry.enabled + + unless current_user || project + return error('forbidden', 403) unless scope + end + + { token: authorized_token(scope).encoded } + end + + def self.full_access_token(*names) + registry = Gitlab.config.registry + token = JSONWebToken::RSAToken.new(registry.key) + token.issuer = registry.issuer + token.audience = AUDIENCE + token.expire_time = token_expire_at + token[:access] = names.map do |name| + { type: 'repository', name: name, actions: %w(*) } + end + token.encoded + end + + private + + def authorized_token(*accesses) + token = JSONWebToken::RSAToken.new(registry.key) + token.issuer = registry.issuer + token.audience = params[:service] + token.subject = current_user.try(:username) + token.expire_time = ContainerRegistryAuthenticationService.token_expire_at + token[:access] = accesses.compact + token + end + + def scope + return unless params[:scope] + + @scope ||= process_scope(params[:scope]) + end + + def process_scope(scope) + type, name, actions = scope.split(':', 3) + actions = actions.split(',') + return unless type == 'repository' + + process_repository_access(type, name, actions) + end + + def process_repository_access(type, name, actions) + requested_project = Project.find_with_namespace(name) + return unless requested_project + + actions = actions.select do |action| + can_access?(requested_project, action) + end + + { type: type, name: name, actions: actions } if actions.present? + end + + def can_access?(requested_project, requested_action) + return false unless requested_project.container_registry_enabled? + + case requested_action + when 'pull' + requested_project == project || can?(current_user, :read_container_image, requested_project) + when 'push' + requested_project == project || can?(current_user, :create_container_image, requested_project) + else + false + end + end + + def registry + Gitlab.config.registry + end + + def self.token_expire_at + Time.now + current_application_settings.container_registry_token_expire_delay.minutes + end + end +end diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb index 18274ce24e2..64bcdac5c65 100644 --- a/app/services/ci/create_builds_service.rb +++ b/app/services/ci/create_builds_service.rb @@ -1,11 +1,11 @@ module Ci class CreateBuildsService - def initialize(commit) - @commit = commit + def initialize(pipeline) + @pipeline = pipeline end def execute(stage, user, status, trigger_request = nil) - builds_attrs = config_processor.builds_for_stage_and_ref(stage, @commit.ref, @commit.tag, trigger_request) + builds_attrs = config_processor.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request) # check when to create next build builds_attrs = builds_attrs.select do |build_attrs| @@ -21,8 +21,8 @@ module Ci builds_attrs.map do |build_attrs| # don't create the same build twice - unless @commit.builds.find_by(ref: @commit.ref, tag: @commit.tag, - trigger_request: trigger_request, name: build_attrs[:name]) + unless @pipeline.builds.find_by(ref: @pipeline.ref, tag: @pipeline.tag, + trigger_request: trigger_request, name: build_attrs[:name]) build_attrs.slice!(:name, :commands, :tag_list, @@ -31,13 +31,13 @@ module Ci :stage, :stage_idx) - build_attrs.merge!(ref: @commit.ref, - tag: @commit.tag, + build_attrs.merge!(ref: @pipeline.ref, + tag: @pipeline.tag, trigger_request: trigger_request, user: user, - project: @commit.project) + project: @pipeline.project) - @commit.builds.create!(build_attrs) + @pipeline.builds.create!(build_attrs) end end end @@ -45,7 +45,7 @@ module Ci private def config_processor - @config_processor ||= @commit.config_processor + @config_processor ||= @pipeline.config_processor end end end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb new file mode 100644 index 00000000000..a7751b8effc --- /dev/null +++ b/app/services/ci/create_pipeline_service.rb @@ -0,0 +1,50 @@ +module Ci + class CreatePipelineService < BaseService + def execute + pipeline = project.pipelines.new(params) + + unless ref_names.include?(params[:ref]) + pipeline.errors.add(:base, 'Reference not found') + return pipeline + end + + unless commit + pipeline.errors.add(:base, 'Commit not found') + return pipeline + end + + unless can?(current_user, :create_pipeline, project) + pipeline.errors.add(:base, 'Insufficient permissions to create a new pipeline') + return pipeline + end + + begin + Ci::Pipeline.transaction do + pipeline.sha = commit.id + + unless pipeline.config_processor + pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file') + raise ActiveRecord::Rollback + end + + pipeline.save! + pipeline.create_builds(current_user) + end + rescue + pipeline.errors.add(:base, 'The pipeline could not be created. Please try again.') + end + + pipeline + end + + private + + def ref_names + @ref_names ||= project.repository.ref_names + end + + def commit + @commit ||= project.commit(params[:ref]) + end + end +end diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index 993acf11db9..1e629cf119a 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -7,14 +7,14 @@ module Ci # check if ref is tag tag = project.repository.find_tag(ref).present? - ci_commit = project.ci_commits.create(sha: commit.sha, ref: ref, tag: tag) + pipeline = project.pipelines.create(sha: commit.sha, ref: ref, tag: tag) trigger_request = trigger.trigger_requests.create!( variables: variables, - commit: ci_commit, + pipeline: pipeline, ) - if ci_commit.create_builds(nil, trigger_request) + if pipeline.create_builds(nil, trigger_request) trigger_request end end diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb index 3018f27ec05..75d847d5bee 100644 --- a/app/services/ci/image_for_build_service.rb +++ b/app/services/ci/image_for_build_service.rb @@ -3,9 +3,9 @@ module Ci def execute(project, opts) sha = opts[:sha] || ref_sha(project, opts[:ref]) - ci_commits = project.ci_commits.where(sha: sha) - ci_commits = ci_commits.where(ref: opts[:ref]) if opts[:ref] - image_name = image_for_status(ci_commits.status) + pipelines = project.pipelines.where(sha: sha) + pipelines = pipelines.where(ref: opts[:ref]) if opts[:ref] + image_name = image_for_status(pipelines.status) image_path = Rails.root.join('public/ci', image_name) OpenStruct.new(path: image_path, name: image_name) diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb index 0d2aa1ff03d..418f5cf8091 100644 --- a/app/services/create_commit_builds_service.rb +++ b/app/services/create_commit_builds_service.rb @@ -18,26 +18,23 @@ class CreateCommitBuildsService return false end - commit = project.ci_commit(sha, ref) - unless commit - commit = project.ci_commits.new(sha: sha, ref: ref, before_sha: before_sha, tag: tag) + pipeline = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag) - # Skip creating ci_commit when no gitlab-ci.yml is found - unless commit.ci_yaml_file - return false - end - - # Create a new ci_commit - commit.save! + # Skip creating pipeline when no gitlab-ci.yml is found + unless pipeline.ci_yaml_file + return false end + # Create a new pipeline + pipeline.save! + # Skip creating builds for commits that have [ci skip] - unless commit.skip_ci? + unless pipeline.skip_ci? # Create builds for commit - commit.create_builds(user) + pipeline.create_builds(user) end - commit.touch - commit + pipeline.touch + pipeline end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 66136b62617..a886f35981f 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -53,10 +53,6 @@ class GitPushService < BaseService # could cause the last commit of a merge request to change. update_merge_requests - # Checks if the main language has changed in the project and if so - # it updates it accordingly - update_main_language - perform_housekeeping end @@ -64,19 +60,6 @@ class GitPushService < BaseService @project.repository.copy_gitattributes(params[:ref]) end - def update_main_language - # Performance can be bad so for now only check main_language once - # See https://gitlab.com/gitlab-org/gitlab-ce/issues/14937 - return if @project.main_language.present? - - return unless is_default_branch? - return unless push_to_new_branch? || push_to_existing_branch? - - current_language = @project.repository.main_language - @project.update_attributes(main_language: current_language) - true - end - protected def update_merge_requests diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 7410442609d..299a0a967b0 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -23,7 +23,7 @@ class GitTagPushService < BaseService commits = [] message = nil - if !Gitlab::Git.blank_ref?(params[:newrev]) + unless Gitlab::Git.blank_ref?(params[:newrev]) tag_name = Gitlab::Git.ref_name(params[:ref]) tag = project.repository.find_tag(tag_name) if tag && tag.target == params[:newrev] diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 2b16089df1b..e3dc569152c 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -45,6 +45,8 @@ class IssuableBaseService < BaseService unless can?(current_user, ability, project) params.delete(:milestone_id) + params.delete(:add_label_ids) + params.delete(:remove_label_ids) params.delete(:label_ids) params.delete(:assignee_id) end @@ -67,10 +69,34 @@ class IssuableBaseService < BaseService end def filter_labels - return if params[:label_ids].to_a.empty? + if params[:add_label_ids].present? || params[:remove_label_ids].present? + params.delete(:label_ids) + + filter_labels_in_param(:add_label_ids) + filter_labels_in_param(:remove_label_ids) + else + filter_labels_in_param(:label_ids) + end + end + + def filter_labels_in_param(key) + return if params[key].to_a.empty? - params[:label_ids] = - project.labels.where(id: params[:label_ids]).pluck(:id) + params[key] = project.labels.where(id: params[key]).pluck(:id) + end + + def update_issuable(issuable, attributes) + issuable.with_transaction_returning_status do + add_label_ids = attributes.delete(:add_label_ids) + remove_label_ids = attributes.delete(:remove_label_ids) + + issuable.label_ids |= add_label_ids if add_label_ids + issuable.label_ids -= remove_label_ids if remove_label_ids + + issuable.assign_attributes(attributes.merge(updated_by: current_user)) + + issuable.save + end end def update(issuable) @@ -78,7 +104,7 @@ class IssuableBaseService < BaseService filter_params old_labels = issuable.labels.to_a - if params.present? && issuable.update_attributes(params.merge(updated_by: current_user)) + if params.present? && update_issuable(issuable, params) issuable.reset_events_cache handle_common_system_notes(issuable, old_labels: old_labels) handle_changes(issuable, old_labels: old_labels) diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb index de8387c4900..15825b81685 100644 --- a/app/services/issues/bulk_update_service.rb +++ b/app/services/issues/bulk_update_service.rb @@ -4,9 +4,9 @@ module Issues issues_ids = params.delete(:issues_ids).split(",") issue_params = params - issue_params.delete(:state_event) unless issue_params[:state_event].present? - issue_params.delete(:milestone_id) unless issue_params[:milestone_id].present? - issue_params.delete(:assignee_id) unless issue_params[:assignee_id].present? + %i(state_event milestone_id assignee_id add_label_ids remove_label_ids).each do |key| + issue_params.delete(key) unless issue_params[key].present? + end issues = Issue.where(id: issues_ids) issues.each do |issue| diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index e61628086f0..ab667456db7 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -24,6 +24,7 @@ module Issues @new_issue = create_new_issue rewrite_notes + rewrite_award_emoji add_note_moved_from # Old issue tasks @@ -72,6 +73,14 @@ module Issues end end + def rewrite_award_emoji + @old_issue.award_emoji.each do |award| + new_award = award.dup + new_award.awardable = @new_issue + new_award.save + end + end + def rewrite_content(content) return unless content diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 3563cbaa997..c7d406cc331 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -24,6 +24,10 @@ module Issues todo_service.reassigned_issue(issue, current_user) end + if issue.previous_changes.include?('confidential') + create_confidentiality_note(issue) + end + added_labels = issue.labels - old_labels if added_labels.present? notification_service.relabeled_issue(issue, added_labels, current_user) @@ -37,5 +41,11 @@ module Issues def close_service Issues::CloseService end + + private + + def create_confidentiality_note(issue) + SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user) + end end end diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb new file mode 100644 index 00000000000..566049525cb --- /dev/null +++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb @@ -0,0 +1,17 @@ +module MergeRequests + class AddTodoWhenBuildFailsService < MergeRequests::BaseService + # Adds a todo to the parent merge_request when a CI build fails + def execute(commit_status) + each_merge_request(commit_status) do |merge_request| + todo_service.merge_request_build_failed(merge_request) + end + end + + # Closes any pending build failed todos for the parent MRs when a build is retried + def close(commit_status) + each_merge_request(commit_status) do |merge_request| + todo_service.merge_request_build_retried(merge_request) + end + end + end +end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index e6837a18696..bc93ba2552d 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -38,5 +38,30 @@ module MergeRequests def filter_params super(:merge_request) end + + def merge_request_from(commit_status) + branches = commit_status.ref + + # This is for ref-less builds + branches ||= @project.repository.branch_names_contains(commit_status.sha) + + return [] if branches.blank? + + merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a + merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a + + merge_requests.uniq.select(&:source_project) + end + + def each_merge_request(commit_status) + merge_request_from(commit_status).each do |merge_request| + pipeline = merge_request.pipeline + + next unless pipeline + next unless pipeline.sha == commit_status.sha + + yield merge_request, pipeline + end + end end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index cd4230aa5e4..1b48899bb0a 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -66,7 +66,7 @@ module MergeRequests commits = merge_request.compare_commits if commits && commits.count == 1 commit = commits.first - merge_request.title = commit.title + merge_request.title = commit.title merge_request.description ||= commit.description.try(:strip) elsif iid && (issue = merge_request.target_project.get_issue(iid)) && !issue.try(:confidential?) case issue diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index 33609d01f20..96a25330af1 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -8,11 +8,14 @@ module MergeRequests @project = Project.find(params[:target_project_id]) if params[:target_project_id] filter_params - label_params = params[:label_ids] - merge_request = MergeRequest.new(params.except(:label_ids)) + label_params = params.delete(:label_ids) + force_remove_source_branch = params.delete(:force_remove_source_branch) + + merge_request = MergeRequest.new(params) merge_request.source_project = source_project merge_request.target_project ||= source_project merge_request.author = current_user + merge_request.merge_params['force_remove_source_branch'] = force_remove_source_branch if merge_request.save merge_request.update_attributes(label_ids: label_params) diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 9a58383b398..9aaf5a5e561 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -45,10 +45,14 @@ module MergeRequests def after_merge MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) - if params[:should_remove_source_branch].present? - DeleteBranchService.new(@merge_request.source_project, current_user). + if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch? + DeleteBranchService.new(@merge_request.source_project, branch_deletion_user). execute(merge_request.source_branch) end end + + def branch_deletion_user + @merge_request.force_remove_source_branch? ? @merge_request.author : current_user + end end end diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb index d6af12f9739..12edfb2d671 100644 --- a/app/services/merge_requests/merge_when_build_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb @@ -20,16 +20,10 @@ module MergeRequests # Triggers the automatic merge of merge_request once the build succeeds def trigger(commit_status) - merge_requests = merge_request_from(commit_status) - - merge_requests.each do |merge_request| + each_merge_request(commit_status) do |merge_request, pipeline| next unless merge_request.merge_when_build_succeeds? next unless merge_request.mergeable? - - ci_commit = merge_request.ci_commit - next unless ci_commit - next unless ci_commit.sha == commit_status.sha - next unless ci_commit.success? + next unless pipeline.success? MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) end @@ -47,20 +41,5 @@ module MergeRequests end end - private - - def merge_request_from(commit_status) - branches = commit_status.ref - - # This is for ref-less builds - branches ||= @project.repository.branch_names_contains(commit_status.sha) - - return [] if branches.blank? - - merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a - merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a - - merge_requests.uniq.select(&:source_project) - end end end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 8b3d56c2b4c..fe0579744b4 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -12,6 +12,7 @@ module MergeRequests close_merge_requests reload_merge_requests reset_merge_when_build_succeeds + mark_pending_todos_done # Leave a system note if a branch was deleted/added if branch_added? || branch_removed? @@ -80,6 +81,12 @@ module MergeRequests merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds) end + def mark_pending_todos_done + merge_requests_for_source_branch.each do |merge_request| + todo_service.merge_request_push(merge_request, @current_user) + end + end + def find_new_commits if branch_added? @commits = [] diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 477c64e7377..026a37997d4 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -11,6 +11,8 @@ module MergeRequests params.except!(:target_project_id) params.except!(:source_branch) + merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) + update(merge_request) end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 01586994813..02fca5c0ea3 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -5,7 +5,12 @@ module Notes note.author = current_user note.system = false - return unless valid_project?(note) + 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) + end if note.save # Finish the harder work in the background @@ -15,14 +20,5 @@ module Notes note end - - private - - def valid_project?(note) - return false unless project - return true if note.for_commit? - - note.noteable.try(:project) == project - end end end diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb index e818f58d13c..534c48aefff 100644 --- a/app/services/notes/post_process_service.rb +++ b/app/services/notes/post_process_service.rb @@ -8,7 +8,7 @@ module Notes def execute # Skip system notes, like status changes and cross-references and awards - unless @note.system || @note.is_award + unless @note.system? EventCreateService.new.leave_note(@note, @note.author) @note.create_cross_references! execute_note_hooks diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 42ec1ac9e1a..91ca82ed3b7 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -130,8 +130,7 @@ class NotificationService # ignore gitlab service messages return true if note.note.start_with?('Status changed to closed') - return true if note.cross_reference? && note.system == true - return true if note.is_award + return true if note.cross_reference? && note.system? target = note.noteable diff --git a/app/services/oauth2/access_token_validation_service.rb b/app/services/oauth2/access_token_validation_service.rb index 6194f6ce91e..264fdccde8f 100644 --- a/app/services/oauth2/access_token_validation_service.rb +++ b/app/services/oauth2/access_token_validation_service.rb @@ -22,6 +22,7 @@ module Oauth2::AccessTokenValidationService end protected + # True if the token's scope is a superset of required scopes, # or the required scopes is empty. def sufficient_scope?(token, scopes) diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index ba50305dbd5..eb73948006e 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -4,6 +4,10 @@ module Projects @project.issues.visible_to_user(current_user).opened.select([:iid, :title]) end + def milestones + @project.milestones.active.reorder(due_date: :asc, title: :asc).select([:iid, :title]) + end + def merge_requests @project.merge_requests.opened.select([:iid, :title]) end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 501e58c1407..61cac5419ad 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -6,6 +6,7 @@ module Projects def execute forked_from_project_id = params.delete(:forked_from_project_id) + import_data = params.delete(:import_data) @project = Project.new(params) @@ -49,22 +50,20 @@ module Projects @project.build_forked_project_link(forked_from_project_id: forked_from_project_id) end - Project.transaction do - @project.save + save_project_and_import_data(import_data) - if @project.persisted? && !@project.import? - raise 'Failed to create repository' unless @project.create_repository - end - end + @project.import_start if @project.import? after_create_actions if @project.persisted? + if @project.errors.empty? + @project.add_import_job if @project.import? + else + fail(error: @project.errors.full_messages.join(', ')) + end @project rescue => e - message = "Unable to save project: #{e.message}" - Rails.logger.error(message) - @project.errors.add(:base, message) if @project - @project + fail(error: e.message) end protected @@ -93,8 +92,30 @@ module Projects unless @project.group @project.team << [current_user, :master, current_user] end + end - @project.import_start if @project.import? + def save_project_and_import_data(import_data) + Project.transaction do + @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data + + if @project.save && !@project.import? + raise 'Failed to create repository' unless @project.create_repository + end + end + end + + def fail(error:) + message = "Unable to save project. Error: #{error}" + message << "Project ID: #{@project.id}" if @project && @project.id + + Rails.logger.error(message) + + if @project && @project.import? + @project.errors.add(:base, message) + @project.mark_import_as_failed(message) + end + + @project end end end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 19aab999e00..f09072975c3 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -26,6 +26,10 @@ module Projects Project.transaction do project.destroy! + unless remove_registry_tags + raise_error('Failed to remove project container registry. Please try again or contact administrator') + end + unless remove_repository(repo_path) raise_error('Failed to remove project repository. Please try again or contact administrator') end @@ -35,7 +39,7 @@ module Projects end end - log_info("Project \"#{project.name}\" was removed") + log_info("Project \"#{project.path_with_namespace}\" was removed") system_hook_service.execute_hooks_for(project, :destroy) true end @@ -59,6 +63,12 @@ module Projects end end + def remove_registry_tags + return true unless Gitlab.config.registry.enabled + + project.container_registry_repository.delete_tags + end + def raise_error(message) raise DestroyError.new(message) end diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 0577ae778d5..de6dc38cc8e 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -3,7 +3,7 @@ module Projects def execute new_params = { forked_from_project_id: @project.id, - visibility_level: @project.visibility_level, + visibility_level: allowed_visibility_level, description: @project.description, name: @project.name, path: @project.path, @@ -19,5 +19,17 @@ module Projects new_project = CreateService.new(current_user, new_params).execute new_project end + + private + + def allowed_visibility_level + project_level = @project.visibility_level + + if Gitlab::VisibilityLevel.non_restricted_level?(project_level) + project_level + else + Gitlab::VisibilityLevel.highest_allowed_level + end + end end end diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb index 3b7c36f0908..43db29315a1 100644 --- a/app/services/projects/housekeeping_service.rb +++ b/app/services/projects/housekeeping_service.rb @@ -22,7 +22,7 @@ module Projects end def execute - raise LeaseTaken if !try_obtain_lease + raise LeaseTaken unless try_obtain_lease GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace) ensure diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 1a23a4ede97..25524c1c060 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -2,36 +2,39 @@ module Projects module ImportExport class ExportService < BaseService - def execute(options = {}) + def execute(_options = {}) @shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.path_with_namespace, 'work')) - save_all if [save_version, save_project_tree, save_uploads, bundle_repo, bundle_wiki_repo].all? - cleanup_and_notify_worker if @shared.errors.any? + save_all end private - def save_version - Gitlab::ImportExport::VersionSaver.save(shared: @shared) + def save_all + if [version_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save) + Gitlab::ImportExport::Saver.save(shared: @shared) + else + cleanup_and_notify_worker + end end - def save_project_tree - Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared).save + def version_saver + Gitlab::ImportExport::VersionSaver.new(shared: @shared) end - def save_uploads - Gitlab::ImportExport::UploadsSaver.save(project: project, shared: @shared) + def project_tree_saver + Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared) end - def bundle_repo - Gitlab::ImportExport::RepoBundler.new(project: project, shared: @shared).bundle + def uploads_saver + Gitlab::ImportExport::UploadsSaver.new(project: project, shared: @shared) end - def bundle_wiki_repo - Gitlab::ImportExport::WikiRepoBundler.new(project: project, shared: @shared).bundle + def repo_saver + Gitlab::ImportExport::RepoSaver.new(project: project, shared: @shared) end - def save_all - Gitlab::ImportExport::Saver.save(shared: @shared) + def wiki_repo_saver + Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared) end def cleanup_and_notify_worker diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index ef15ef6a473..c4838d31f2f 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -39,7 +39,7 @@ module Projects begin gitlab_shell.import_repository(project.path_with_namespace, project.import_url) rescue Gitlab::Shell::Error => e - raise Error, e.message + raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}" end end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 111b3ec05ea..03b57dea51e 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -34,6 +34,11 @@ module Projects raise TransferError.new("Project with same path in target namespace already exists") end + if project.has_container_registry_tags? + # we currently doesn't support renaming repository if it contains tags in container registry + raise TransferError.new('Project cannot be transferred, because tags are present in its container registry') + end + project.expire_caches_before_rename(old_path) # Apply new namespace id and visibility level diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index e43b5b51e5b..1fb72cf89e9 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -85,7 +85,7 @@ class SystemHooksService path_with_namespace: model.path_with_namespace, project_id: model.id, owner_name: owner.name, - owner_email: owner.respond_to?(:email) ? owner.email : "", + owner_email: owner.respond_to?(:email) ? owner.email : "", project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase } end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 4bdb1b0c074..4e8fa0818b9 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -169,12 +169,33 @@ class SystemNoteService # # Returns the created Note object def self.change_title(noteable, project, author, old_title) - return unless noteable.respond_to?(:title) + new_title = noteable.title.dup - body = "Title changed from **#{old_title}** to **#{noteable.title}**" + old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs + + marked_old_title = Gitlab::Diff::InlineDiffMarker.new(old_title).mark(old_diffs, mode: :deletion, markdown: true) + marked_new_title = Gitlab::Diff::InlineDiffMarker.new(new_title).mark(new_diffs, mode: :addition, markdown: true) + + body = "Changed title: **#{marked_old_title}** → **#{marked_new_title}**" create_note(noteable: noteable, project: project, author: author, note: body) end + # Called when the confidentiality changes + # + # issue - Issue object + # project - Project owning the issue + # author - User performing the change + # + # Example Note text: + # + # "Made the issue confidential" + # + # Returns the created Note object + def self.change_issue_confidentiality(issue, project, author) + body = issue.confidential ? 'Made the issue confidential' : 'Made the issue visible' + create_note(noteable: issue, project: project, author: author, note: body) + end + # Called when a branch in Noteable is changed # # noteable - Noteable object diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 42c5bca90fd..8e03ff8ddde 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -20,7 +20,7 @@ class TodoService # * mark all pending todos related to the issue for the current user as done # def update_issue(issue, current_user) - create_mention_todos(issue.project, issue, current_user) + update_issuable(issue, current_user) end # When close an issue we should: @@ -53,7 +53,7 @@ class TodoService # * create a todo for each mentioned user on merge request # def update_merge_request(merge_request, current_user) - create_mention_todos(merge_request.project, merge_request, current_user) + update_issuable(merge_request, current_user) end # When close a merge request we should: @@ -80,6 +80,30 @@ class TodoService mark_pending_todos_as_done(merge_request, current_user) end + # When a build fails on the HEAD of a merge request we should: + # + # * create a todo for that user to fix it + # + def merge_request_build_failed(merge_request) + create_build_failed_todo(merge_request) + end + + # When a new commit is pushed to a merge request we should: + # + # * mark all pending todos related to the merge request for that user as done + # + def merge_request_push(merge_request, current_user) + mark_pending_todos_as_done(merge_request, current_user) + end + + # When a build is retried to a merge request we should: + # + # * mark all pending todos related to the merge request for the author as done + # + def merge_request_build_retried(merge_request) + mark_pending_todos_as_done(merge_request, merge_request.author) + end + # When create a note we should: # # * mark all pending todos related to the noteable for the note author as done @@ -98,6 +122,14 @@ class TodoService handle_note(note, current_user) end + # When an emoji is awarded we should: + # + # * mark all pending todos related to the awardable for the current user as done + # + def new_award_emoji(awardable, current_user) + mark_pending_todos_as_done(awardable, current_user) + end + # When marking pending todos as done we should: # # * mark all pending todos related to the target for the current user as done @@ -121,6 +153,13 @@ class TodoService create_mention_todos(issuable.project, issuable, author) end + def update_issuable(issuable, author) + # Skip toggling a task list item in a description + return if issuable.tasks? && issuable.updated_tasks.any? + + create_mention_todos(issuable.project, issuable, author) + end + def handle_note(note, author) # Skip system notes, and notes on project snippet return if note.system? || note.for_snippet? @@ -145,6 +184,12 @@ class TodoService create_todos(mentioned_users, attributes) end + def create_build_failed_todo(merge_request) + author = merge_request.author + attributes = attributes_for_todo(merge_request.project, merge_request, author, Todo::BUILD_FAILED) + create_todos(author, attributes) + end + def attributes_for_target(target) attributes = { project_id: target.project.id, diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb index 9162f128602..4c0a2c6b4d8 100644 --- a/app/services/wiki_pages/base_service.rb +++ b/app/services/wiki_pages/base_service.rb @@ -6,9 +6,8 @@ module WikiPages object_kind: page.class.name.underscore, user: current_user.hook_attrs, project: @project.hook_attrs, - object_attributes: page.hook_attrs, - # DEPRECATED - repository: @project.hook_attrs.slice(:name, :url, :description, :homepage) + wiki: @project.wiki.hook_attrs, + object_attributes: page.hook_attrs } page_url = Gitlab::UrlBuilder.build(page) diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml index 2ab01704b77..862b86d9d4a 100644 --- a/app/views/admin/abuse_reports/_abuse_report.html.haml +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -16,7 +16,7 @@ .light.small = time_ago_with_tooltip(abuse_report.created_at) %td - = markdown(abuse_report.message.squish!, pipeline: :single_line) + = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter) %td - if user = link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true), diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index e0d8d16a954..c883e8f97da 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -106,9 +106,22 @@ .form-group .col-sm-offset-2.col-sm-10 .checkbox + = f.label :send_user_confirmation_email do + = f.check_box :send_user_confirmation_email + Send confirmation email on sign-up + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox = f.label :signin_enabled do = f.check_box :signin_enabled Sign-in enabled + - if omniauth_enabled? && button_based_providers.any? + .form-group + = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth Sign-In sources', class: 'control-label col-sm-2' + .col-sm-10 + .btn-group{ data: { toggle: 'buttons' } } + - oauth_providers_checkboxes.each do |source| + = source .form-group = f.label :two_factor_authentication, 'Two-factor authentication', class: 'control-label col-sm-2' .col-sm-10 @@ -142,6 +155,11 @@ = f.text_area :sign_in_text, class: 'form-control', rows: 4 .help-block Markdown enabled .form-group + = f.label :after_sign_up_text, class: 'control-label col-sm-2' + .col-sm-10 + = f.text_area :after_sign_up_text, class: 'form-control', rows: 4 + .help-block Markdown enabled + .form-group = f.label :help_page_text, class: 'control-label col-sm-2' .col-sm-10 = f.text_area :help_page_text, class: 'form-control', rows: 4 @@ -165,6 +183,14 @@ .col-sm-10 = f.number_field :max_artifacts_size, class: 'form-control' + - if Gitlab.config.registry.enabled + %fieldset + %legend Container Registry + .form-group + = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :container_registry_token_expire_delay, class: 'form-control' + %fieldset %legend Metrics %p diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index 804d7851bdb..d74cf8598e8 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -20,7 +20,7 @@ = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post .row-content-block.second-block - #{(@scope || 'running').capitalize} builds + #{(@scope || 'all').capitalize} builds %ul.content-list - if @builds.blank? @@ -47,4 +47,3 @@ = render "admin/builds/build", build: build = paginate @builds, theme: 'gitlab' - diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml new file mode 100644 index 00000000000..c2313986a7f --- /dev/null +++ b/app/views/admin/health_check/show.html.haml @@ -0,0 +1,49 @@ +- page_title "Health Check" + +%h3.page-title + Health Check +.bs-callout.clearfix + .pull-left + %p + Access token is + %code#health-check-token= current_application_settings.health_check_access_token + = button_to reset_health_check_token_admin_application_settings_path, + method: :put, class: 'btn btn-default', + data: { confirm: 'Are you sure you want to reset the health check token?' } do + = icon('refresh') + Reset health check access token +%p.light + Health information can be retrieved as plain text, JSON, or XML using: + %ul + %li + %code= health_check_url(token: current_application_settings.health_check_access_token) + %li + %code= health_check_url(token: current_application_settings.health_check_access_token, format: :json) + %li + %code= health_check_url(token: current_application_settings.health_check_access_token, format: :xml) + +%p.light + You can also ask for the status of specific services: + %ul + %li + %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :cache) + %li + %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :database) + %li + %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :migrations) + +%hr +.panel.panel-default + .panel-heading + Current Status: + - if @errors.blank? + = icon('circle', class: 'cgreen') + Healthy + - else + = icon('warning', class: 'cred') + Unhealthy + .panel-body + - if @errors.blank? + No Health Problems Detected + - else + = @errors diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml index 6745e58deca..36b21eefdee 100644 --- a/app/views/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -11,18 +11,10 @@ = link_to admin_runner_path(runner) do = runner.short_sha %td - .runner-description - = runner.description - %span (#{link_to 'edit', '#', class: 'edit-runner-link'}) - .runner-description-form.hide - = form_for [:admin, runner], remote: true, html: { class: 'form-inline' } do |f| - .form-group - = f.text_field :description, class: 'form-control' - = f.submit 'Save', class: 'btn' - %span (#{link_to 'cancel', '#', class: 'cancel'}) + = runner.description %td - if runner.shared? - \- + n/a - else = runner.projects.count(:all) %td diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 8700b4820cd..e049b40bfab 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -9,8 +9,6 @@ %span.runner-state.runner-state-specific Specific - - - if @runner.shared? .bs-callout.bs-callout-success %h4 This runner will process builds from ALL UNASSIGNED projects @@ -22,25 +20,9 @@ %h4 This runner will process builds only from ASSIGNED projects %p You can't make this a shared runner. %hr -= form_for @runner, url: admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f| - .form-group - = label_tag :token, class: 'control-label' do - Token - .col-sm-10 - = f.text_field :token, class: 'form-control', readonly: true - .form-group - = label_tag :description, class: 'control-label' do - Description - .col-sm-10 - = f.text_field :description, class: 'form-control' - .form-group - = label_tag :tag_list, class: 'control-label' do - Tags - .col-sm-10 - = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control' - .help-block You can setup builds to only use runners with specific tags - .form-actions - = f.submit 'Save', class: 'btn btn-save' + +.append-bottom-20 + = render '/projects/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner) .row .col-md-6 @@ -117,8 +99,8 @@ %td.build-link - if project - = link_to ci_status_path(build.commit) do - %strong #{build.commit.short_sha} + = link_to ci_status_path(build.pipeline) do + %strong #{build.pipeline.short_sha} %td.timestamp - if build.finished_at diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml new file mode 100644 index 00000000000..02efcecc889 --- /dev/null +++ b/app/views/award_emoji/_awards_block.html.haml @@ -0,0 +1,15 @@ +- grouped_emojis = awardable.grouped_awards(with_thumbs: inline) +.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } } + - awards_sort(grouped_emojis).each do |emoji, awards| + %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } } + = emoji_icon(emoji, sprite: false) + %span.award-control-text.js-counter + = awards.count + + - if current_user + .award-menu-holder.js-award-holder + %button.btn.award-control.js-add-award{ type: "button" } + = icon('smile-o', class: "award-control-icon award-control-icon-normal") + = icon('spinner spin', class: "award-control-icon award-control-icon-loading") + %span.award-control-text + Add diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index 3d17f74b709..23c145ebbb4 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -9,5 +9,4 @@ - if current_user.can_create_group? .nav-controls = link_to new_group_path, class: "btn btn-new" do - = icon('plus') New Group diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 9da3fcbd986..d35f332e1e0 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -18,5 +18,4 @@ = render 'shared/projects/dropdown' - if current_user.can_create_project? = link_to new_project_path, class: 'btn btn-new' do - = icon('plus') New Project diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder index 0d7b1b30dc3..83c0c6da21b 100644 --- a/app/views/dashboard/issues.atom.builder +++ b/app/views/dashboard/issues.atom.builder @@ -6,8 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id issues_dashboard_url xml.updated @issues.first.created_at.xmlschema if @issues.any? - @issues.each do |issue| - issue_to_atom(xml, issue) - end + xml << render(partial: 'issues/issue', collection: @issues) if @issues.any? end - diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder index d4daf07c6c0..fb5be63b472 100644 --- a/app/views/dashboard/projects/index.atom.builder +++ b/app/views/dashboard/projects/index.atom.builder @@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id dashboard_projects_url xml.updated @events[0].updated_at.xmlschema if @events[0] - @events.each do |event| - event_to_atom(xml, event) - end + xml << render(partial: 'events/event', collection: @events) if @events.any? end diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index aa0aff86d4d..98f302d2f93 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -1,13 +1,15 @@ %li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} } .todo-item.todo-block = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:'' - .todo-title.title - %span.author-name - - if todo.author - = link_to_author(todo) - - else - (removed) + - unless todo.build_failed? + = todo_target_state_pill(todo) + + %span.author-name + - if todo.author + = link_to_author(todo) + - else + (removed) %span.todo-label = todo_action_name(todo) - if todo.target diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml index 3c3830a3f10..73c3a3dd2eb 100644 --- a/app/views/devise/confirmations/almost_there.haml +++ b/app/views/devise/confirmations/almost_there.haml @@ -3,6 +3,9 @@ Almost there... %p.lead Please check your email to confirm your account +- if after_sign_up_text.present? + .well-confirmation.text-center + = markdown(after_sign_up_text) %p.confirmation-content.text-center No confirmation email received? Please check your spam folder or .append-bottom-20.prepend-top-20.text-center diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb deleted file mode 100644 index c6fa8f0ee36..00000000000 --- a/app/views/devise/mailer/confirmation_instructions.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -<p>Welcome <%= @resource.name %>!</p> - -<% if @resource.unconfirmed_email.present? %> - <p>You can confirm your email (<%= @resource.unconfirmed_email %>) through the link below:</p> -<% else %> - <p>You can confirm your account through the link below:</p> -<% end %> - -<p><%= link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) %></p> diff --git a/app/views/devise/mailer/confirmation_instructions.html.haml b/app/views/devise/mailer/confirmation_instructions.html.haml new file mode 100644 index 00000000000..086bb8e083d --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.haml @@ -0,0 +1,16 @@ +.center + - if @resource.unconfirmed_email.present? + #content + %h2= @resource.unconfirmed_email + %p Click the link below to confirm your email address. + #cta + = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token) + - else + #content + - if Gitlab.com? + %h2 Thanks for signing up to GitLab! + - else + %h2 Welcome, #{@resource.name}! + %p To get started, click the link below to confirm your account. + #cta + = link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) diff --git a/app/views/devise/mailer/confirmation_instructions.text.erb b/app/views/devise/mailer/confirmation_instructions.text.erb new file mode 100644 index 00000000000..9f76edb76a4 --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.text.erb @@ -0,0 +1,9 @@ +Welcome, <%= @resource.name %>! + +<% if @resource.unconfirmed_email.present? %> +You can confirm your email (<%= @resource.unconfirmed_email %>) through the link below: +<% else %> +You can confirm your account through the link below: +<% end %> + +<%= confirmation_url(@resource, confirmation_token: @token) %> diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index d65fa60025c..28194506acc 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -4,7 +4,7 @@ = render 'devise/shared/signin_box' -# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box - - if omniauth_enabled? && devise_mapping.omniauthable? + - if omniauth_enabled? && devise_mapping.omniauthable? && button_based_providers_enabled? .clearfix.prepend-top-20 = render 'devise/shared/omniauth_box' diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index c9d1e454a5e..a373f61bd3c 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -1,10 +1,19 @@ %div .login-box .login-heading - %h3 Two-factor Authentication + %h3 Two-Factor Authentication .login-body - = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| - = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor Authentication code', required: true, autofocus: true - %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. - .prepend-top-20 - = f.submit "Verify code", class: "btn btn-save" + - if @user.two_factor_otp_enabled? + %h5 Authenticate via Two-Factor App + = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| + - resource_params = params[resource_name].presence || params + = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) + = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-Factor Authentication code', required: true, autofocus: true, autocomplete: 'off' + %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. + .prepend-top-20 + = f.submit "Verify code", class: "btn btn-save" + + - if @user.two_factor_u2f_enabled? + + %hr + = render "u2f/authenticate" diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index ecf680e7b23..de18bc2d844 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,7 +1,7 @@ %p %span.light Sign in with - - providers = button_based_providers + - providers = enabled_button_based_providers - providers.each do |provider| %span.light - has_icon = provider_has_icon?(provider) diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 510215bb8cd..905a8dbcd84 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -16,7 +16,7 @@ %div = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true .form-group.append-bottom-20#password-strength - = f.password_field :password, class: "form-control bottom", placeholder: "Password", required: true + = f.password_field :password, class: "form-control bottom", placeholder: "Password - minimum length #{@minimum_password_length} characters", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters" %div - if current_application_settings.recaptcha_enabled = recaptcha_tags diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index eae80e5210f..ce050007204 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -1,4 +1,4 @@ -%h3.page-title Authorize required +%h3.page-title Authorization required %main{:role => "main"} %p.h4 Authorize diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml index 3443a8e2307..97401a2e618 100644 --- a/app/views/emojis/index.html.haml +++ b/app/views/emojis/index.html.haml @@ -1,9 +1,9 @@ .emoji-menu .emoji-menu-content = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control" - - AwardEmoji.emoji_by_category.each do |category, emojis| + - Gitlab::AwardEmoji.emoji_by_category.each do |category, emojis| %h5.emoji-menu-title - = AwardEmoji::CATEGORIES[category] + = Gitlab::AwardEmoji::CATEGORIES[category] %ul.clearfix.emoji-menu-list - emojis.each do |emoji| %li.pull-left.text-center.emoji-menu-list-item diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml index dce4081288c..1bc9f604438 100644 --- a/app/views/events/_commit.html.haml +++ b/app/views/events/_commit.html.haml @@ -2,4 +2,4 @@ .commit-row-title = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: '', title: truncate_sha(commit[:id]) · - = markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line + = markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line, author: event.author diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder new file mode 100644 index 00000000000..7890e717aa7 --- /dev/null +++ b/app/views/events/_event.atom.builder @@ -0,0 +1,20 @@ +return unless event.visible_to_user?(current_user) + +xml.entry do + xml.id "tag:#{request.host},#{event.created_at.strftime("%Y-%m-%d")}:#{event.id}" + xml.link href: event_feed_url(event) + xml.title truncate(event_feed_title(event), length: 80) + xml.updated event.created_at.xmlschema + xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email)) + + xml.author do + xml.name event.author_name + xml.email event.author_email + end + + xml.summary(type: "xhtml") do |summary| + event_summary = event_feed_summary(event) + + summary << event_summary unless event_summary.nil? + end +end diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml index fad65310021..083c3936212 100644 --- a/app/views/events/_event_issue.atom.haml +++ b/app/views/events/_event_issue.atom.haml @@ -1,2 +1,2 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - = markdown(issue.description, pipeline: :atom, project: issue.project) + = markdown(issue.description, pipeline: :atom, project: issue.project, author: issue.author) diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml index 19bdc7b9ca5..d7e05600627 100644 --- a/app/views/events/_event_merge_request.atom.haml +++ b/app/views/events/_event_merge_request.atom.haml @@ -1,2 +1,2 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - = markdown(merge_request.description, pipeline: :atom, project: merge_request.project) + = markdown(merge_request.description, pipeline: :atom, project: merge_request.project, author: merge_request.author) diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml index b730ebbd5f9..1154f982821 100644 --- a/app/views/events/_event_note.atom.haml +++ b/app/views/events/_event_note.atom.haml @@ -1,2 +1,2 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - = markdown(note.note, pipeline: :atom, project: note.project) + = markdown(note.note, pipeline: :atom, project: note.project, author: note.author) diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index b271b9daff1..28bee1d0a33 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -6,7 +6,7 @@ %i at = commit[:timestamp].to_time.to_s(:short) - %blockquote= markdown(escape_once(commit[:message]), pipeline: :atom, project: event.project) + %blockquote= markdown(escape_once(commit[:message]), pipeline: :atom, project: event.project, author: event.author) - if event.commits_count > 15 %p %i diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index c994e3b997d..2e2403347c1 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -1,10 +1,14 @@ .event-title %span.author_name= link_to_author event %span.event_label{class: event.action_name} - = event_action_name(event) - - if event.target - %strong= link_to event.target.reference_link_text, [event.project.namespace.becomes(Namespace), event.project, event.target] + = event.action_name + %strong + = link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title do + = event.target_type.titleize.downcase + = event.target.reference_link_text + - else + = event_action_name(event) = event_preposition(event) diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 235bd46107e..dc4ff17e31a 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -15,7 +15,7 @@ %ul.well-list.event_commits - few_commits = event.commits[0...2] - few_commits.each do |commit| - = render "events/commit", commit: commit, project: project + = render "events/commit", commit: commit, project: project, event: event - create_mr = event.new_ref? && create_mr_button?(event.project.default_branch, event.ref_name, event.project) - if event.commits_count > 1 diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml index dc76599b776..71cc4d87b1f 100644 --- a/app/views/groups/_activities.html.haml +++ b/app/views/groups/_activities.html.haml @@ -4,7 +4,7 @@ .nav-block - if current_user .controls - = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do + = link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn' do %i.fa.fa-rss = render 'shared/event_filter' diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 60234be8f83..6bb542e658d 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -34,9 +34,9 @@ %strong.member-access-level= member.human_access - if show_controls - if can?(current_user, :update_group_member, member) - = button_tag class: "btn-xs btn js-toggle-button", + = button_tag class: "btn-xs btn btn-grouped inline js-toggle-button", title: 'Edit access level', type: 'button' do - %i.fa.fa-pencil-square-o + = icon('pencil') - if can?(current_user, :destroy_group_member, member) @@ -46,7 +46,7 @@ Leave - else = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do - %i.fa.fa-minus.fa-inverse + = icon('trash') .edit-member.hide.js-toggle-content %br diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder index 486d1d8587a..c19671295af 100644 --- a/app/views/groups/issues.atom.builder +++ b/app/views/groups/issues.atom.builder @@ -1,13 +1,10 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do - xml.title "#{@user.name} issues" - xml.link href: issues_dashboard_url(format: :atom, private_token: @user.private_token), rel: "self", type: "application/atom+xml" - xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" - xml.id issues_dashboard_url + xml.title "#{@group.name} issues" + xml.link href: issues_group_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" + xml.link href: issues_group_url, rel: "alternate", type: "text/html" + xml.id issues_group_url xml.updated @issues.first.created_at.xmlschema if @issues.any? - @issues.each do |issue| - issue_to_atom(xml, issue) - end + xml << render(partial: 'issues/issue', collection: @issues) if @issues.any? end - diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index 7d9d27ae1fc..ca6c4326d1c 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -39,9 +39,8 @@ .col-md-6 .form-group = f.label :due_date, "Due Date", class: "control-label" - .col-sm-10= f.hidden_field :due_date .col-sm-10 - .datepicker + = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date" .form-actions = f.submit 'Create Milestone', class: "btn-create btn" diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index c66b82bb484..b68bf444d27 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id group_url(@group) xml.updated @events[0].updated_at.xmlschema if @events[0] - @events.each do |event| - event_to_atom(xml, event) - end + xml << render(@events) if @events.any? end diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 77c297255b8..85635bc4616 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -5,7 +5,7 @@ = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") .cover-block.groups-cover-block - .container-fluid.container-limited + %div{ class: (container_class) } = link_to group_icon(@group), target: '_blank' do = image_tag group_icon(@group), class: "avatar group-avatar s70" .group-info @@ -35,7 +35,6 @@ = render 'shared/projects/dropdown' - if can? current_user, :create_projects, @group = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do - = icon('plus') New Project .tab-content diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 70e88da7aae..01648047ce2 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -24,7 +24,7 @@ %td Show/hide this dialog %tr %td.shortcut - - if browser.mac? + - if browser.platform.mac? .key ⌘ shift p - else .key ctrl shift p diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 5b7f11440c1..6c4a9d68d1f 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -4,6 +4,10 @@ %i.fa.fa-github Import projects from GitHub +%p + %i.fa.fa-warning + To import GitHub pull requests, any pull request source branches that had been deleted are temporarily restored on GitHub. To prevent any connected CI services from being overloaded with dozens of irrelevant branches being created and deleted again, GitHub webhooks are temporarily disabled during the import process. + %p.light Select projects you want to import. %hr diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index e3a356b5379..aedb8468eca 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -47,7 +47,7 @@ %td.import-target = repo["path_with_namespace"] %td.import-actions.job-status - = button_tag class: "btn js-add-to-import" do + = button_tag class: "btn btn-import js-add-to-import" do Import = icon("spinner spin", class: "loading-icon") diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder new file mode 100644 index 00000000000..68a2d19e58d --- /dev/null +++ b/app/views/issues/_issue.atom.builder @@ -0,0 +1,14 @@ +xml.entry do + xml.id namespace_project_issue_url(issue.project.namespace, issue.project, issue) + xml.link href: namespace_project_issue_url(issue.project.namespace, issue.project, issue) + xml.title truncate(issue.title, length: 80) + xml.updated issue.created_at.xmlschema + xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email)) + + xml.author do |author| + xml.name issue.author_name + xml.email issue.author_email + end + + xml.summary issue.title +end diff --git a/app/views/kaminari/gitlab/_first_page.html.haml b/app/views/kaminari/gitlab/_first_page.html.haml index ada7306d98d..e7a70e3bb28 100644 --- a/app/views/kaminari/gitlab/_first_page.html.haml +++ b/app/views/kaminari/gitlab/_first_page.html.haml @@ -2,7 +2,7 @@ -# available local variables -# url: url to the first page -# current_page: a page object for the currently displayed page --# num_pages: total number of pages +-# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote %li.first diff --git a/app/views/kaminari/gitlab/_gap.html.haml b/app/views/kaminari/gitlab/_gap.html.haml index 3ffd12f8587..80ca30f36e6 100644 --- a/app/views/kaminari/gitlab/_gap.html.haml +++ b/app/views/kaminari/gitlab/_gap.html.haml @@ -1,7 +1,7 @@ -# Non-link tag that stands for skipped pages... -# available local variables -# current_page: a page object for the currently displayed page --# num_pages: total number of pages +-# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote %li{class: "page"} diff --git a/app/views/kaminari/gitlab/_last_page.html.haml b/app/views/kaminari/gitlab/_last_page.html.haml index 3431d029bcc..53f780d1d1b 100644 --- a/app/views/kaminari/gitlab/_last_page.html.haml +++ b/app/views/kaminari/gitlab/_last_page.html.haml @@ -2,7 +2,7 @@ -# available local variables -# url: url to the last page -# current_page: a page object for the currently displayed page --# num_pages: total number of pages +-# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote %li.last diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml index c805914fc3f..125f09777ba 100644 --- a/app/views/kaminari/gitlab/_next_page.html.haml +++ b/app/views/kaminari/gitlab/_next_page.html.haml @@ -2,7 +2,7 @@ -# available local variables -# url: url to the next page -# current_page: a page object for the currently displayed page --# num_pages: total number of pages +-# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote - if current_page.last? diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml index a52d883b9a8..522e4d1d05f 100644 --- a/app/views/kaminari/gitlab/_page.html.haml +++ b/app/views/kaminari/gitlab/_page.html.haml @@ -3,7 +3,7 @@ -# page: a page object for "this" page -# url: url to this page -# current_page: a page object for the currently displayed page --# num_pages: total number of pages +-# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote %li{class: "page#{' active' if page.current?}"} diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml index a12c53bcfe7..f5e0d2ed3f3 100644 --- a/app/views/kaminari/gitlab/_paginator.html.haml +++ b/app/views/kaminari/gitlab/_paginator.html.haml @@ -1,7 +1,7 @@ -# The container tag -# available local variables -# current_page: a page object for the currently displayed page --# num_pages: total number of pages +-# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -# paginator: the paginator that renders the pagination tags inside @@ -9,7 +9,7 @@ %div.gl-pagination %ul.pagination.clearfix - unless current_page.first? - = first_page_tag unless num_pages < 5 # As kaminari will always show the first 5 pages + = first_page_tag unless total_pages < 5 # As kaminari will always show the first 5 pages = prev_page_tag - each_page do |page| - if page.left_outer? || page.right_outer? || page.inside_window? @@ -18,5 +18,5 @@ = gap_tag = next_page_tag - unless current_page.last? - = last_page_tag unless num_pages < 5 + = last_page_tag unless total_pages < 5 diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml index afb20455e0a..7edf10498a8 100644 --- a/app/views/kaminari/gitlab/_prev_page.html.haml +++ b/app/views/kaminari/gitlab/_prev_page.html.haml @@ -2,7 +2,7 @@ -# available local variables -# url: url to the previous page -# current_page: a page object for the currently displayed page --# num_pages: total number of pages +-# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote - if current_page.first? diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml index 2ed51d87ca1..e4fab897377 100644 --- a/app/views/layouts/_collapse_button.html.haml +++ b/app/views/layouts/_collapse_button.html.haml @@ -1,4 +1 @@ -- if nav_menu_collapsed? - = link_to icon('angle-right'), '#', class: 'toggle-nav-collapse', title: "Open/Close" -- else - = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Open/Close" += link_to icon('bars'), '#', class: 'toggle-nav-collapse', title: "Open/Close" diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 79cdbac1f37..e0ed657919e 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -30,9 +30,10 @@ = javascript_include_tag "application" - = csrf_meta_tags + - if page_specific_javascripts + = javascript_include_tag page_specific_javascripts, {"data-turbolinks-track" => true} - = include_gon + = csrf_meta_tags - unless browser.safari? %meta{name: 'referrer', content: 'origin-when-cross-origin'} diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 3c3bc41bf0e..f89e8582792 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,12 +1,5 @@ -.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } - = render "layouts/broadcast" +.page-with-sidebar.page-sidebar-collapsed{ class: "#{page_gutter_class}" } .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } - .header-logo - %a#logo - = brand_header_logo - = link_to root_path, class: 'gitlab-text-container-link', title: 'Dashboard', id: 'js-shortcuts-home' do - .gitlab-text-container - %h3 GitLab - if defined?(sidebar) && sidebar = render "layouts/nav/#{sidebar}" @@ -18,7 +11,7 @@ .collapse-nav = render partial: 'layouts/collapse_button' - if current_user - = link_to current_user, class: 'sidebar-user', title: "Profile" do + = link_to current_user, class: 'sidebar-user', title: "Profile", data: {user: current_user.username} do = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' .username = current_user.username @@ -26,7 +19,8 @@ .layout-nav .container-fluid = render "layouts/nav/#{nav}" - .content-wrapper{ class: ('page-with-layout-nav' if defined?(nav) && nav) } + .content-wrapper{ class: "#{layout_nav_class}" } + = render "layouts/broadcast" = render "layouts/flash" = yield :flash_message %div{ class: (container_class unless @no_container) } diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 6b208c3d0bb..b49207fc315 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -6,11 +6,8 @@ .search.search-form{class: "#{'has-location-badge' if label.present?}"} = form_tag search_path, method: :get, class: 'navbar-form' do |f| .search-input-container - .search-location-badge - - if label.present? - %span.location-badge - %i.location-text - = label + - if label.present? + .location-badge= label .search-input-wrap .dropdown{ data: {url: search_autocomplete_path } } = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' } diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index e4d1c773d03..2b86b289bbe 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -2,6 +2,8 @@ %html{ lang: "en"} = render "layouts/head" %body{class: "#{user_application_theme}", 'data-page' => body_data_page} + = Gon::Base.render_data + -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body. = yield :scripts_body_top diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml index a13241bebee..2e56d0ac6a3 100644 --- a/app/views/layouts/ci/_page.html.haml +++ b/app/views/layouts/ci/_page.html.haml @@ -1,12 +1,6 @@ .page-with-sidebar{ class: page_sidebar_class } = render "layouts/broadcast" .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } - .header-logo - %a#logo - = brand_header_logo - = link_to root_path, class: 'gitlab-text-container-link', title: 'Dashboard', id: 'js-shortcuts-home' do - .gitlab-text-container - %h3 GitLab - if defined?(sidebar) && sidebar = render "layouts/ci/#{sidebar}" diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index f08cb0a5428..3d28eec84ef 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -2,6 +2,7 @@ %html{ lang: "en"} = render "layouts/head" %body.ui_charcoal.login-page.application.navless + = Gon::Base.render_data = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml index 7c061dd531f..6bd427b02ac 100644 --- a/app/views/layouts/devise_empty.html.haml +++ b/app/views/layouts/devise_empty.html.haml @@ -2,6 +2,7 @@ %html{ lang: "en"} = render "layouts/head" %body.ui_charcoal.login-page.application.navless + = Gon::Base.render_data = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container diff --git a/app/views/layouts/devise_mailer.html.haml b/app/views/layouts/devise_mailer.html.haml new file mode 100644 index 00000000000..c258eafdd51 --- /dev/null +++ b/app/views/layouts/devise_mailer.html.haml @@ -0,0 +1,34 @@ +!!! 5 +%html + %head + %meta(content='text/html; charset=UTF-8' http-equiv='Content-Type') + = stylesheet_link_tag 'mailers/devise' + + %body + %table#wrapper + %tr + %td + %table#header + %td{valign: "top"} + = image_tag('mailers/gitlab_header_logo.png', id: 'logo', alt: 'GitLab Wordmark') + + %table#body + %tr + %td#body-container + = yield + + - if Gitlab.com? + %table#footer + %tr + %td#tanuki + = image_tag('mailers/gitlab_tanuki_2x.png', alt: 'GitLab Logo') + %tr + %td#tagline + Everyone can contribute + %tr + %td#social + = link_to 'Blog', 'https://about.gitlab.com/blog/' + = link_to 'Twitter', 'https://twitter.com/gitlab' + = link_to 'Facebook', 'https://www.facebook.com/gitlab/' + = link_to 'YouTube', 'https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg' + = link_to 'LinkedIn', 'https://www.linkedin.com/company/gitlab-com' diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 915acc4612e..7fbe065df00 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -2,6 +2,7 @@ %html{ lang: "en"} = render "layouts/head" %body{class: "#{user_application_theme} application navless"} + = Gon::Base.render_data = render "layouts/header/empty" .container.navless-container = render "layouts/flash" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 86930d4eaaf..ad30a367fc5 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,9 +1,12 @@ -%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class } +%header.navbar.navbar-fixed-top.navbar-gitlab.header-collapsed{ class: nav_header_class } %div{ class: fluid_layout ? "container-fluid" : "container-fluid" } .header-content - %button.navbar-toggle{type: 'button'} + %button.side-nav-toggle{type: 'button'} %span.sr-only Toggle navigation = icon('bars') + %button.navbar-toggle{type: 'button'} + %span.sr-only Toggle navigation + = icon('angle-left') .navbar-collapse.collapse %ul.nav.navbar-nav @@ -24,8 +27,9 @@ %li = link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('bell fw') - %span.badge.todos-pending-count - = todos_pending_count + - unless todos_pending_count == 0 + %span.badge.todos-pending-count + = todos_pending_count - if current_user.can_create_project? %li = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do @@ -46,6 +50,10 @@ %h1.title= title + .header-logo + #logo + = brand_header_logo + = yield :header_content = render 'shared/outdated_browser' diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 280a1b93729..f292730fe45 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -41,6 +41,11 @@ = icon('file-text fw') %span Logs + = nav_link(controller: :health_check) do + = link_to admin_health_check_path, title: 'Health Check' do + = icon('medkit fw') + %span + Health Check = nav_link(controller: :broadcast_messages) do = link_to admin_broadcast_messages_path, title: 'Messages' do = icon('bullhorn fw') diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index fad4224e945..18cae5bf87f 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,7 +1,7 @@ %ul.nav.nav-sidebar - = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: 'home'}) do - = link_to dashboard_projects_path, title: 'Projects' do - = icon('bookmark fw') + = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do + = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do + = navbar_icon('project') %span Projects = nav_link(controller: :todos) do @@ -9,31 +9,31 @@ = icon('bell fw') %span Todos - %span.count.todos-pending-count= number_with_delimiter(todos_pending_count) + %span.count= number_with_delimiter(todos_pending_count) = nav_link(path: 'dashboard#activity') do - = link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity' do - = icon('dashboard fw') + = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do + = navbar_icon('activity') %span Activity = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = link_to dashboard_groups_path, title: 'Groups' do - = icon('group fw') + = navbar_icon('group') %span Groups = nav_link(controller: 'dashboard/milestones') do = link_to dashboard_milestones_path, title: 'Milestones' do - = icon('clock-o fw') + = navbar_icon('milestones') %span Milestones = nav_link(path: 'dashboard#issues') do - = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues' do - = icon('exclamation-circle fw') + = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do + = navbar_icon('issues') %span Issues %span.count= number_with_delimiter(current_user.assigned_issues.opened.count) = nav_link(path: 'dashboard#merge_requests') do - = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do - = icon('tasks fw') + = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do + = navbar_icon('mr') %span Merge Requests %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) @@ -47,7 +47,6 @@ = icon('question-circle fw') %span Help - = nav_link(html_options: {class: profile_tab_class}) do = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do = icon('user fw') diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 3438005863a..66361a644dd 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,37 +1,34 @@ -= render 'layouts/nav/group_settings' +%div{ class: nav_control_class } + = render 'layouts/nav/group_settings' -%ul.nav-links - = nav_link(path: 'groups#show', html_options: {class: 'home'}) do - = link_to group_path(@group), title: 'Home' do - = icon('group fw') - %span - Group - = nav_link(path: 'groups#activity') do - = link_to activity_group_path(@group), title: 'Activity' do - = icon('dashboard fw') - %span - Activity - = nav_link(controller: [:group, :milestones]) do - = link_to group_milestones_path(@group), title: 'Milestones' do - = icon('clock-o fw') - %span - Milestones - = nav_link(path: 'groups#issues') do - = link_to issues_group_path(@group), title: 'Issues' do - = icon('exclamation-circle fw') - %span - Issues - - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - %span.badge.count= number_with_delimiter(issues.count) - = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group), title: 'Merge Requests' do - = icon('tasks fw') - %span - Merge Requests - - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute - %span.badge.count= number_with_delimiter(merge_requests.count) - = nav_link(controller: [:group_members]) do - = link_to group_group_members_path(@group), title: 'Members' do - = icon('users fw') - %span - Members + %ul.nav-links.scrolling-tabs + .fade-left + = nav_link(path: 'groups#show', html_options: {class: 'home'}) do + = link_to group_path(@group), title: 'Home' do + %span + Group + = nav_link(path: 'groups#activity') do + = link_to activity_group_path(@group), title: 'Activity' do + %span + Activity + = nav_link(controller: [:group, :milestones]) do + = link_to group_milestones_path(@group), title: 'Milestones' do + %span + Milestones + = nav_link(path: 'groups#issues') do + = link_to issues_group_path(@group), title: 'Issues' do + %span + Issues + - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute + %span.badge.count= number_with_delimiter(issues.count) + = nav_link(path: 'groups#merge_requests') do + = link_to merge_requests_group_path(@group), title: 'Merge Requests' do + %span + Merge Requests + - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute + %span.badge.count= number_with_delimiter(merge_requests.count) + = nav_link(controller: [:group_members]) do + = link_to group_group_members_path(@group), title: 'Members' do + %span + Members + .fade-right diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index e391ec7f2b7..0b2673f1a82 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -1,7 +1,7 @@ - if current_user - if access = @group.users.find_by(id: current_user.id) .controls - %span.dropdown.group-settings-dropdown + .dropdown.group-settings-dropdown %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} = icon('cog') = icon('caret-down') diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index d730840d63a..d4b1f477f3f 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,49 +1,42 @@ -%ul.nav-links +%ul.nav-links.scrolling-tabs + .fade-left = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile Settings' do - = icon('user fw') %span Profile = nav_link(controller: [:accounts, :two_factor_auths]) do = link_to profile_account_path, title: 'Account' do - = icon('gear fw') %span Account - = nav_link(controller: 'oauth/applications') do - = link_to applications_profile_path, title: 'Applications' do - = icon('cloud fw') - %span - Applications + - if current_application_settings.user_oauth_applications? + = nav_link(controller: 'oauth/applications') do + = link_to applications_profile_path, title: 'Applications' do + %span + Applications = nav_link(controller: :emails) do = link_to profile_emails_path, title: 'Emails' do - = icon('envelope-o fw') %span Emails - unless current_user.ldap_user? = nav_link(controller: :passwords) do = link_to edit_profile_password_path, title: 'Password' do - = icon('lock fw') %span Password = nav_link(controller: :notifications) do = link_to profile_notifications_path, title: 'Notifications' do - = icon('inbox fw') %span Notifications = nav_link(controller: :keys) do = link_to profile_keys_path, title: 'SSH Keys' do - = icon('key fw') %span SSH Keys = nav_link(controller: :preferences) do = link_to profile_preferences_path, title: 'Preferences' do - -# TODO (rspeicher): Better icon? - = icon('image fw') %span Preferences = nav_link(path: 'profiles#audit_log') do = link_to audit_log_profile_path, title: 'Audit Log' do - = icon('history fw') %span Audit Log + .fade-right diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 479bde33719..53d1fcc30a6 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,131 +1,108 @@ -%ul.nav.nav-sidebar - - if @project.group - = nav_link do - = link_to group_path(@project.group), title: 'Go to group', class: 'back-link' do - = icon('caret-square-o-left fw') +- if current_user + .controls + - access = user_max_access_in_project(current_user.id, @project) + - can_edit = can?(current_user, :admin_project, @project) + .dropdown.project-settings-dropdown + %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', 'data-toggle' => 'dropdown'} + = icon('cog') + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-align-right + = render 'layouts/nav/project_settings' + %li.divider + - if can_edit + %li + = link_to edit_project_path(@project) do + Edit Project + - if access + %li + = link_to leave_namespace_project_project_members_path(@project.namespace, @project), + data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do + Leave Project + +%div{ class: nav_control_class } + %ul.nav-links.scrolling-tabs + .fade-left + = nav_link(path: 'projects#show', html_options: {class: 'home'}) do + = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do %span - Go to group - - else - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard - - %li.separate-item - - = nav_link(path: 'projects#show', html_options: {class: 'home'}) do - = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do - = icon('bookmark fw') - %span - Project - = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do - = icon('dashboard fw') - %span - Activity - - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do - = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do - = icon('files-o fw') - %span - Files + Project - - if project_nav_tab? :commits - = nav_link(controller: %w(commit commits compare repositories tags branches releases network)) do - = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do - = icon('history fw') + = nav_link(path: 'projects#activity') do + = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do %span - Commits + Activity + + - if project_nav_tab? :files + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do + = link_to project_files_path(@project), title: 'Code', class: 'shortcuts-tree' do + %span + Code + + - if project_nav_tab? :pipelines + = nav_link(controller: :pipelines) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + %span + Pipelines + + - if project_nav_tab? :container_registry + = nav_link(controller: %w(container_registry)) do + = link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do + %span + Registry + + - if project_nav_tab? :graphs + = nav_link(controller: %w(graphs)) do + = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do + %span + Graphs + + - if project_nav_tab? :issues + = nav_link(controller: [:issues, :labels, :milestones]) do + = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do + %span + Issues + - if @project.default_issues_tracker? + %span.badge.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count) + + - if project_nav_tab? :merge_requests + = nav_link(controller: :merge_requests) do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do + %span + Merge Requests + %span.badge.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) + + - if project_nav_tab? :wiki + = nav_link(controller: :wikis) do + = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do + %span + Wiki + + - if project_nav_tab? :snippets + = nav_link(controller: :snippets) do + = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do + %span + Snippets + + -# Global shortcut to network page for compatibility + - if project_nav_tab? :network + %li.hidden + = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do + Network + + -# Shortcut to create a new issue + %li.hidden + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do + Create a new issue - - if project_nav_tab? :builds - = nav_link(controller: %w(builds)) do - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do - = icon('cubes fw') - %span + -# Shortcut to builds page + - if project_nav_tab? :builds + %li.hidden + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do Builds - %span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) - - - if project_nav_tab? :graphs - = nav_link(controller: %w(graphs)) do - = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do - = icon('area-chart fw') - %span - Graphs - - - if project_nav_tab? :milestones - = nav_link(controller: :milestones) do - = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do - = icon('clock-o fw') - %span - Milestones - - - if project_nav_tab? :issues - = nav_link(controller: :issues) do - = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do - = icon('exclamation-circle fw') - %span - Issues - - if @project.default_issues_tracker? - %span.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count) - - - if project_nav_tab? :merge_requests - = nav_link(controller: :merge_requests) do - = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do - = icon('tasks fw') - %span - Merge Requests - %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) - - if project_nav_tab? :team - = nav_link(controller: [:project_members, :teams]) do - = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do - = icon('users fw') - %span - Members - - - if project_nav_tab? :labels - = nav_link(controller: :labels) do - = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do - = icon('tags fw') - %span - Labels - - - if project_nav_tab? :wiki - = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do - = icon('book fw') - %span - Wiki - - - if project_nav_tab? :forks - = nav_link(controller: :forks, action: :index) do - = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks' do - = icon('code-fork fw') - %span - Forks - - - if project_nav_tab? :snippets - = nav_link(controller: :snippets) do - = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do - = icon('clipboard fw') - %span - Snippets - - - if project_nav_tab? :settings - = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do - = link_to edit_project_path(@project), title: 'Settings' do - = icon('cogs fw') - %span - Settings - - -# Global shortcut to network page for compatibility - - if project_nav_tab? :network - %li.hidden - = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do - Network - - -# Shortcut to create a new issue - %li.hidden - = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do - Create a new issue + -# Shortcut to commits page + - if project_nav_tab? :commits + %li.hidden + = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do + Commits + .fade-right diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index d429a928464..885e78d38c6 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -1,63 +1,45 @@ -%ul.nav.nav-sidebar - = nav_link do - = link_to project_path(@project), title: 'Go to project', class: 'back-link' do - = icon('caret-square-o-left fw') +- if project_nav_tab? :team + = nav_link(controller: [:project_members, :teams]) do + = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do %span - Go to project + Members - %li.separate-item - - %ul.sidebar-subnav - = nav_link(path: 'projects#edit') do - = link_to edit_project_path(@project), title: 'Project Settings' do - = icon('pencil-square-o fw') - %span - Project Settings - - if @project.allowed_to_share_with_group? - = nav_link(controller: :group_links) do - = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do - = icon('share-square-o fw') - %span - Groups - = nav_link(controller: :deploy_keys) do - = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do - = icon('key fw') - %span - Deploy Keys - = nav_link(controller: :hooks) do - = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do - = icon('link fw') - %span - Webhooks - = nav_link(controller: :services) do - = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do - = icon('cogs fw') - %span - Services - = nav_link(controller: :protected_branches) do - = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do - = icon('lock fw') - %span - Protected Branches +- if @project.allowed_to_share_with_group? + = nav_link(controller: :group_links) do + = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do + %span + Groups += nav_link(controller: :deploy_keys) do + = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do + %span + Deploy Keys += nav_link(controller: :hooks) do + = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do + %span + Webhooks += nav_link(controller: :services) do + = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do + %span + Services += nav_link(controller: :protected_branches) do + = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do + %span + Protected Branches - - if @project.builds_enabled? - = nav_link(controller: :runners) do - = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do - = icon('cog fw') - %span - Runners - = nav_link(controller: :variables) do - = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do - = icon('code fw') - %span - Variables - = nav_link(controller: :triggers) do - = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do - = icon('retweet fw') - %span - Triggers - = nav_link(controller: :badges) do - = link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do - = icon('star-half-empty fw') - %span - Badges +- if @project.builds_enabled? + = nav_link(controller: :runners) do + = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do + %span + Runners + = nav_link(controller: :variables) do + = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do + %span + Variables + = nav_link(controller: :triggers) do + = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do + %span + Triggers + = nav_link(controller: :badges) do + = link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do + %span + Badges diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 2997f59d946..dde2e2889dc 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -4,6 +4,7 @@ %title GitLab = stylesheet_link_tag 'notify' + = yield :head %body %div.content = yield diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 6dfe7fbdae8..2049b204956 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -1,12 +1,12 @@ - page_title @project.name_with_namespace - page_description @project.description unless page_description - header_title project_title(@project) unless header_title -- sidebar "project" unless sidebar +- nav "project" - content_for :scripts_body_top do - project = @target_project || @project - - if @project_wiki - - markdown_preview_path = namespace_project_wikis_markdown_preview_path(project.namespace, project) + - if @project_wiki && @page + - markdown_preview_path = namespace_project_wiki_markdown_preview_path(project.namespace, project, params[:id]) - else - markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project) - if current_user diff --git a/app/views/layouts/project_settings.html.haml b/app/views/layouts/project_settings.html.haml index 59ce38f67bb..4bc94bd132d 100644 --- a/app/views/layouts/project_settings.html.haml +++ b/app/views/layouts/project_settings.html.haml @@ -1,5 +1,4 @@ - page_title "Settings" -- header_title project_title(@project, "Settings", edit_project_path(@project)) -- sidebar "project_settings" +- nav "project" = render template: "layouts/project" diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml index 12ded41fbf2..e9c66170877 100644 --- a/app/views/notify/_note_message.html.haml +++ b/app/views/notify/_note_message.html.haml @@ -2,4 +2,4 @@ %div #{link_to @note.author_name, user_url(@note.author)} wrote: %div - = markdown(@note.note, pipeline: :email) + = markdown(@note.note, pipeline: :email, author: @note.author) diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml index 81d65037312..4bf7c1f4d64 100644 --- a/app/views/notify/build_fail_email.html.haml +++ b/app/views/notify/build_fail_email.html.haml @@ -10,7 +10,7 @@ %p Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)} %p - Author: #{@build.commit.git_author_name} + Author: #{@build.pipeline.git_author_name} %p Branch: #{@build.ref} %p @@ -18,7 +18,7 @@ %p Job: #{@build.name} %p - Message: #{@build.commit.git_commit_message} + Message: #{@build.pipeline.git_commit_message} %p Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} diff --git a/app/views/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb index 675acea60a1..9d497983498 100644 --- a/app/views/notify/build_fail_email.text.erb +++ b/app/views/notify/build_fail_email.text.erb @@ -1,11 +1,11 @@ Build failed for <%= @project.name %> Status: <%= @build.status %> -Commit: <%= @build.commit.short_sha %> -Author: <%= @build.commit.git_author_name %> +Commit: <%= @build.pipeline.short_sha %> +Author: <%= @build.pipeline.git_author_name %> Branch: <%= @build.ref %> Stage: <%= @build.stage %> Job: <%= @build.name %> -Message: <%= @build.commit.git_commit_message %> +Message: <%= @build.pipeline.git_commit_message %> Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %> diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml index 5d247eb4cf2..252a5b7152c 100644 --- a/app/views/notify/build_success_email.html.haml +++ b/app/views/notify/build_success_email.html.haml @@ -10,7 +10,7 @@ %p Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)} %p - Author: #{@build.commit.git_author_name} + Author: #{@build.pipeline.git_author_name} %p Branch: #{@build.ref} %p @@ -18,7 +18,7 @@ %p Job: #{@build.name} %p - Message: #{@build.commit.git_commit_message} + Message: #{@build.pipeline.git_commit_message} %p Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} diff --git a/app/views/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb index 747da44acae..c5ed4f84861 100644 --- a/app/views/notify/build_success_email.text.erb +++ b/app/views/notify/build_success_email.text.erb @@ -1,11 +1,11 @@ Build successful for <%= @project.name %> Status: <%= @build.status %> -Commit: <%= @build.commit.short_sha %> -Author: <%= @build.commit.git_author_name %> +Commit: <%= @build.pipeline.short_sha %> +Author: <%= @build.pipeline.git_author_name %> Branch: <%= @build.ref %> Stage: <%= @build.stage %> Job: <%= @build.name %> -Message: <%= @build.commit.git_commit_message %> +Message: <%= @build.pipeline.git_commit_message %> Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %> diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml index ad3ab2525bb..f42b150c0d6 100644 --- a/app/views/notify/new_issue_email.html.haml +++ b/app/views/notify/new_issue_email.html.haml @@ -2,7 +2,7 @@ %div #{link_to @issue.author_name, user_url(@issue.author)} wrote: -if @issue.description - = markdown(@issue.description, pipeline: :email) + = markdown(@issue.description, pipeline: :email, author: @issue.author) - if @issue.assignee_id.present? %p diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index 23423e7d981..158404de396 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -9,4 +9,4 @@ Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name} -if @merge_request.description - = markdown(@merge_request.description, pipeline: :email) + = markdown(@merge_request.description, pipeline: :email, author: @merge_request.author) diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml index 65f0e4c4068..a3643a00cfe 100644 --- a/app/views/notify/note_merge_request_email.html.haml +++ b/app/views/notify/note_merge_request_email.html.haml @@ -1,7 +1,7 @@ -- if @note.diff_file_name +- if @note.legacy_diff_note? %p.details New comment on diff for - = link_to @note.diff_file_name, @target_url + = link_to @note.diff_file_path, @target_url \: = render 'note_message' diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index f2e405b14fd..f1532371b2e 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -1,3 +1,6 @@ += content_for :head do + = stylesheet_link_tag 'mailers/repository_push_email' + %h3 #{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name} at #{link_to(@message.project_name_with_namespace, namespace_project_url(@message.project_namespace, @message.project))} @@ -43,26 +46,38 @@ = diff.new_path - unless @message.disable_diffs? - %h4 Changes: - - @message.diffs.each_with_index do |diff, i| - %li{id: "diff-#{i}"} - %a{href: @message.target_url + "#diff-#{i}"} - - if diff.deleted_file - %strong - = diff.old_path - deleted - - elsif diff.renamed_file - %strong - = diff.old_path - → - %strong - = diff.new_path - - else - %strong - = diff.new_path - %hr - = color_email_diff(diff.diff) - %br + - diff_files = @message.diffs - - if @message.compare_timeout - %h5 Huge diff. To prevent performance issues changes are hidden + - if @message.compare_timeout + %h5 The diff was not included because it is too large. + - else + %h4 Changes: + - diff_files.each_with_index do |diff_file, i| + %li{id: "diff-#{i}"} + %a{href: @message.target_url + "#diff-#{i}"}< + - if diff_file.deleted_file + %strong< + = diff_file.old_path + deleted + - elsif diff_file.renamed_file + %strong< + = diff_file.old_path + → + %strong< + = diff_file.new_path + - else + %strong< + = diff_file.new_path + - if diff_file.too_large? + The diff for this file was not included because it is too large. + - else + %hr + - diff_commit = diff_file.deleted_file ? @message.diff_refs.first : @message.diff_refs.last + - blob = @message.project.repository.blob_for_diff(diff_commit, diff_file) + - if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob) + %table.code.white + - diff_file.highlighted_diff_lines.each do |line| + = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: nil, plain: true} + - else + No preview for this file type + %br diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml index 53869e36b28..5ac23aa3997 100644 --- a/app/views/notify/repository_push_email.text.haml +++ b/app/views/notify/repository_push_email.text.haml @@ -25,24 +25,28 @@ - else \- #{diff.new_path} - unless @message.disable_diffs? - \ - \ - Changes: - - @message.diffs.each do |diff| + - if @message.compare_timeout \ - \===================================== - - if diff.deleted_file - #{diff.old_path} deleted - - elsif diff.renamed_file - #{diff.old_path} → #{diff.new_path} - - else - = diff.new_path - \===================================== - != diff.diff - - if @message.compare_timeout - \ - \ - Huge diff. To prevent performance issues it was hidden + \ + The diff was not included because it is too large. + - else + \ + \ + Changes: + - @message.diffs.each do |diff_file| + \ + \===================================== + - if diff_file.deleted_file + #{diff_file.old_path} deleted + - elsif diff_file.renamed_file + #{diff_file.old_path} → #{diff_file.new_path} + - else + = diff_file.new_path + \===================================== + - if diff_file.too_large? + The diff for this file was not included because it is too large. + - else + != diff_file.diff.diff - if @message.target_url \ \ diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index afd3d79321f..3d2a245ecbd 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -11,7 +11,7 @@ %p Your private token is used to access application resources without authentication. .col-lg-9 - = form_for @user, url: reset_private_token_profile_path, method: :put, html: {class: "private-token"} do |f| + = form_for @user, url: reset_private_token_profile_path, method: :put, html: { class: "private-token" } do |f| %p.cgray - if current_user.private_token = label_tag "token", "Private token", class: "label-light" @@ -29,21 +29,22 @@ .row.prepend-top-default .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 - Two-factor Authentication + Two-Factor Authentication %p - Increase your account's security by enabling two-factor authentication (2FA). + Increase your account's security by enabling Two-Factor Authentication (2FA). .col-lg-9 %p - Status: #{current_user.two_factor_enabled? ? 'enabled' : 'disabled'} - - if !current_user.two_factor_enabled? - %p - Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code. - More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. - .append-bottom-10 - = link_to 'Enable two-factor authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' + Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'} + - if current_user.two_factor_enabled? + = link_to 'Manage Two-Factor Authentication', profile_two_factor_auth_path, class: 'btn btn-info' + = link_to 'Disable', profile_two_factor_auth_path, + method: :delete, + data: { confirm: "Are you sure? This will invalidate your registered applications and U2F devices." }, + class: 'btn btn-danger' - else - = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-danger', - data: { confirm: 'Are you sure?' } + .append-bottom-10 + = link_to 'Enable Two-Factor Authentication', profile_two_factor_auth_path, class: 'btn btn-success' + %hr - if button_based_providers.any? .row.prepend-top-default @@ -70,7 +71,7 @@ - if current_user.can_change_username? .row.prepend-top-default .col-lg-3.profile-settings-sidebar - %h4.prepend-top-0.change-username-title + %h4.prepend-top-0.warning-title Change username %p Changing your username will change path to all personal projects! @@ -94,7 +95,7 @@ - if signup_enabled? .row.prepend-top-default .col-lg-3.profile-settings-sidebar - %h4.prepend-top-0.remove-account-title + %h4.prepend-top-0.danger-title Remove account .col-lg-9 - if @user.can_be_removed? diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index bfe53be6854..1b1b16d656f 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -5,7 +5,7 @@ %h4.prepend-top-0 Application theme %p - This setting allows you to customize the appearance of the site, ex. sidebar. + This setting allows you to customize the appearance of the site, e.g. the sidebar. .col-lg-9.application-theme - Gitlab::Themes.each do |theme| = label_tag do diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml deleted file mode 100644 index 69fc81cb45c..00000000000 --- a/app/views/profiles/two_factor_auths/new.html.haml +++ /dev/null @@ -1,39 +0,0 @@ -- page_title 'Two-factor Authentication', 'Account' - -.row.prepend-top-default - .col-lg-3 - %h4.prepend-top-0 - Two-factor Authentication (2FA) - %p - Increase your account's security by enabling two-factor authentication (2FA). - .col-lg-9 - %p - Download the Google Authenticator application from App Store for iOS or Google Play for Android and scan this code. - More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. - .row.append-bottom-10 - .col-md-3 - = raw @qr_code - .col-md-9 - .account-well - %p.prepend-top-0.append-bottom-0 - Can't scan the code? - %p.prepend-top-0.append-bottom-0 - To add the entry manually, provide the following details to the application on your phone. - %p.prepend-top-0.append-bottom-0 - Account: - = current_user.email - %p.prepend-top-0.append-bottom-0 - Key: - = current_user.otp_secret.scan(/.{4}/).join(' ') - %p.two-factor-new-manual-content - Time based: Yes - = form_tag profile_two_factor_auth_path, method: :post do |f| - - if @error - .alert.alert-danger - = @error - .form-group - = label_tag :pin_code, nil, class: "label-light" - = text_field_tag :pin_code, nil, class: "form-control", required: true - .prepend-top-default - = submit_tag 'Enable two-factor authentication', class: 'btn btn-success' - = link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable? diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml new file mode 100644 index 00000000000..ce76cb73c9c --- /dev/null +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -0,0 +1,69 @@ +- page_title 'Two-Factor Authentication', 'Account' +- header_title "Two-Factor Authentication", profile_two_factor_auth_path + +.row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0 + Register Two-Factor Authentication App + %p + Use an app on your mobile device to enable two-factor authentication (2FA). + .col-lg-9 + - if current_user.two_factor_otp_enabled? + = icon "check inverse", base: "circle", class: "text-success", text: "You've already enabled two-factor authentication using mobile authenticator applications. You can disable it from your account settings page." + - else + %p + Download the Google Authenticator application from App Store or Google Play Store and scan this code. + More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}. + .row.append-bottom-10 + .col-md-3 + = raw @qr_code + .col-md-9 + .account-well + %p.prepend-top-0.append-bottom-0 + Can't scan the code? + %p.prepend-top-0.append-bottom-0 + To add the entry manually, provide the following details to the application on your phone. + %p.prepend-top-0.append-bottom-0 + Account: + = current_user.email + %p.prepend-top-0.append-bottom-0 + Key: + = current_user.otp_secret.scan(/.{4}/).join(' ') + %p.two-factor-new-manual-content + Time based: Yes + = form_tag profile_two_factor_auth_path, method: :post do |f| + - if @error + .alert.alert-danger + = @error + .form-group + = label_tag :pin_code, nil, class: "label-light" + = text_field_tag :pin_code, nil, class: "form-control", required: true + .prepend-top-default + = submit_tag 'Register with Two-Factor App', class: 'btn btn-success' + +%hr + +.row.prepend-top-default + + .col-lg-3 + %h4.prepend-top-0 + Register Universal Two-Factor (U2F) Device + %p + Use a hardware device to add the second factor of authentication. + %p + As U2F devices are only supported by a few browsers, it's recommended that you set up a + two-factor authentication app as well as a U2F device so you'll always be able to log in + using an unsupported browser. + .col-lg-9 + %p + - if @registration_key_handles.present? + = icon "check inverse", base: "circle", class: "text-success", text: "You have #{pluralize(@registration_key_handles.size, 'U2F device')} registered with GitLab." + - if @u2f_registration.errors.present? + = form_errors(@u2f_registration) + = render "u2f/register" + +- if two_factor_skippable? + :javascript + var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>"; + $(".flash-alert").append(button); + diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml index 0de019983ca..0568c2d305e 100644 --- a/app/views/projects/_builds_settings.html.haml +++ b/app/views/projects/_builds_settings.html.haml @@ -1,74 +1,65 @@ %fieldset.builds-feature - %legend - Builds: - + %h5.prepend-top-0 + Builds - unless @repository.gitlab_ci_yml .form-group - .col-sm-offset-2.col-sm-10 - %p Builds need to be configured before you can begin using Continuous Integration. - = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' - %hr - + %p Builds need to be configured before you can begin using Continuous Integration. + = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' .form-group - .col-sm-offset-2.col-sm-10 - %p Get recent application code using the following command: - .radio - = f.label :build_allow_git_fetch_false do - = f.radio_button :build_allow_git_fetch, 'false' - %strong git clone - %br - %span.descr Slower but makes sure you have a clean dir before every build - .radio - = f.label :build_allow_git_fetch_true do - = f.radio_button :build_allow_git_fetch, 'true' - %strong git fetch - %br - %span.descr Faster + %p Get recent application code using the following command: + .radio + = f.label :build_allow_git_fetch_false do + = f.radio_button :build_allow_git_fetch, 'false' + %strong git clone + %br + %span.descr Slower but makes sure you have a clean dir before every build + .radio + = f.label :build_allow_git_fetch_true do + = f.radio_button :build_allow_git_fetch, 'true' + %strong git fetch + %br + %span.descr Faster .form-group - = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label' - .col-sm-10 - = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0' - %p.help-block per build in minutes + = f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light' + = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0' + %p.help-block per build in minutes .form-group - = f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label' - .col-sm-10 - .input-group - %span.input-group-addon / - = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' - %span.input-group-addon / - %p.help-block - We will use this regular expression to find test coverage output in build trace. - Leave blank if you want to disable this feature - .bs-callout.bs-callout-info - %p Below are examples of regex for existing tools: - %ul - %li - Simplecov (Ruby) - - %code \(\d+.\d+\%\) covered - %li - pytest-cov (Python) - - %code \d+\%\s*$ - %li - phpunit --coverage-text --colors=never (PHP) - - %code ^\s*Lines:\s*\d+.\d+\% - %li - gcovr (C/C++) - - %code ^TOTAL.*\s+(\d+\%)$ - %li - tap --coverage-report=text-summary (Node.js) - - %code ^Statements\s*:\s*([^%]+) + = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light' + .input-group + %span.input-group-addon / + = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' + %span.input-group-addon / + %p.help-block + We will use this regular expression to find test coverage output in build trace. + Leave blank if you want to disable this feature + .bs-callout.bs-callout-info + %p Below are examples of regex for existing tools: + %ul + %li + Simplecov (Ruby) - + %code \(\d+.\d+\%\) covered + %li + pytest-cov (Python) - + %code \d+\%\s*$ + %li + phpunit --coverage-text --colors=never (PHP) - + %code ^\s*Lines:\s*\d+.\d+\% + %li + gcovr (C/C++) - + %code ^TOTAL.*\s+(\d+\%)$ + %li + tap --coverage-report=text-summary (Node.js) - + %code ^Statements\s*:\s*([^%]+) .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :public_builds do - = f.check_box :public_builds - %strong Public builds - .help-block Allow everyone to access builds for Public and Internal projects + .checkbox + = f.label :public_builds do + = f.check_box :public_builds + %strong Public builds + .help-block Allow everyone to access builds for Public and Internal projects - .form-group - = f.label :runners_token, "Runners token", class: 'control-label' - .col-sm-10 - = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89' - %p.help-block The secure token used to checkout project. + .form-group.append-bottom-0 + = f.label :runners_token, "Runners token", class: 'label-light' + = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89' + %p.help-block The secure token used to checkout project. diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 9b5de17dd3b..f5bc1b4e409 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,60 +1,38 @@ - empty_repo = @project.empty_repo? .project-home-panel.cover-block.clearfix{:class => ("empty-project" if empty_repo)} - .project-identicon-holder - = project_icon(@project, alt: '', class: 'project-avatar avatar s90') - .cover-title.project-home-desc - %h1 - = @project.name - %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)} - = visibility_level_icon(@project.visibility_level, fw: false) - - - if @project.description.present? - .cover-desc.project-home-desc - = markdown(@project.description, pipeline: :description) - - - if forked_from_project = @project.forked_from_project - .cover-desc - Forked from - = link_to project_path(forked_from_project) do - = forked_from_project.namespace.try(:name) - - .cover-controls - - if current_user - = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), class: 'btn btn-gray' do - = icon('rss') - - access = user_max_access_in_project(current_user.id, @project) - - can_edit = can?(current_user, :admin_project, @project) - - if access || can_edit - %span.dropdown.project-settings-dropdown - %a.dropdown-new.btn.btn-gray#project-settings-button{href: '#', 'data-toggle' => 'dropdown'} - = icon('cog') - = icon('angle-down') - %ul.dropdown-menu.dropdown-menu-right - - if can_edit - %li - = link_to edit_project_path(@project) do - Edit Project - - if access - %li - = link_to leave_namespace_project_project_members_path(@project.namespace, @project), - data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do - Leave Project - - .project-repo-buttons - .split-one.count-buttons - = render 'projects/buttons/star' - = render 'projects/buttons/fork' - - .clone-row - .project-clone-holder - = render "shared/clone_panel" - - .split-repo-buttons - .btn-group.pull-left - = render "projects/buttons/download" - = render 'projects/buttons/dropdown' - - = render 'projects/buttons/notifications' + %div{ class: (container_class) } + .row + .project-image-container + = project_icon(@project, alt: '', class: 'project-avatar avatar s70') + .project-info + .cover-title.project-home-desc + %h1 + = @project.name + %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)} + = visibility_level_icon(@project.visibility_level, fw: false) + + - if @project.description.present? + .cover-desc.project-home-desc + = markdown(@project.description, pipeline: :description) + + - if forked_from_project = @project.forked_from_project + .cover-desc + Forked from + = link_to project_path(forked_from_project) do + = forked_from_project.namespace.try(:name) + + .project-repo-buttons + .count-buttons + = render 'projects/buttons/star' + = render 'projects/buttons/fork' + + .project-clone-holder + = render "shared/clone_panel" + + .project-repo-buttons.btn-group.project-right-buttons + = render "projects/buttons/download" + = render 'projects/buttons/dropdown' + = render 'projects/buttons/notifications' :javascript new Star(); diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 8de44a6c914..28a28282fd3 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -7,8 +7,14 @@ %li %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } Preview + + - if defined?(@issue) && @issue.confidential? + %li.confidential-issue-warning + = icon('warning') + %span This is a confidential issue. Your comment will not be visible to the public. + %li.pull-right - %button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } + %button.zen-control.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } Go full screen .md-write-holder diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml new file mode 100644 index 00000000000..da522b53417 --- /dev/null +++ b/app/views/projects/_merge_request_settings.html.haml @@ -0,0 +1,11 @@ +%fieldset.builds-feature + %h5.prepend-top-0 + Merge Requests + .form-group + .checkbox + = f.label :only_allow_merge_if_build_succeeds do + = f.check_box :only_allow_merge_if_build_succeeds + %strong Only allow merge requests to be merged if the build succeeds + .help-block + Builds need to be configured to enable this feature. + = link_to icon('question-circle'), help_page_path('workflow', 'merge_requests#only-allow-merge-requests-to-be-merged-if-the-build-succeeds') diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index e1e35013968..413477a2d3a 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -4,5 +4,5 @@ = f.text_area attr, class: classes, placeholder: placeholder - else = text_area_tag attr, nil, class: classes, placeholder: placeholder - %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" } + %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" } = icon('compress') diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml index 69fa4ad37c4..3c0f01cbf6f 100644 --- a/app/views/projects/activity.html.haml +++ b/app/views/projects/activity.html.haml @@ -1,5 +1,4 @@ - page_title "Activity" -- header_title project_title(@project, "Activity", activity_project_path(@project)) = render 'projects/last_push' diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index 49f95ff37db..539d07d634a 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -1,5 +1,5 @@ - page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds' -= render 'projects/builds/header_title' +- header_title project_title(@project, "Builds", project_builds_path(@project)) .top-block.row-content-block.clearfix .pull-right diff --git a/app/views/projects/badges/index.html.haml b/app/views/projects/badges/index.html.haml index c22384ddf46..ee63bc55a30 100644 --- a/app/views/projects/badges/index.html.haml +++ b/app/views/projects/badges/index.html.haml @@ -1,6 +1,5 @@ - page_title 'Badges' - badges_path = namespace_project_badges_path(@project.namespace, @project) -- header_title project_title(@project, 'Badges', badges_path) .prepend-top-10 .panel.panel-default diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 5f9a92ff93f..377665b096f 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -1,5 +1,4 @@ - page_title "Blame", @blob.path, @ref -- header_title project_title(@project, "Files", project_files_path(@project)) %h3.page-title Blame view diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index fefa652a3da..4071b59c003 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -16,6 +16,9 @@ .license-selector.js-license-selector.hide = select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name} + .gitignore-selector.hidden + = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { filenames: gitignore_names } } ) + .encoding-selector = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' diff --git a/app/views/projects/blob/_header_title.html.haml b/app/views/projects/blob/_header_title.html.haml deleted file mode 100644 index 78c5ef20a5f..00000000000 --- a/app/views/projects/blob/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Files", project_files_path(@project)) diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index effcce5a1c4..e4f04ca7764 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Edit", @blob.path, @ref -= render "header_title" .file-editor %ul.nav-links.no-bottom.js-edit-mode diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 0459699432e..c952bc7e5db 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,5 +1,4 @@ - page_title "New File", @path.presence, @ref -= render "header_title" %h3.page-title New File diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index 6988039b6c7..ed670dae88d 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -1,5 +1,4 @@ - page_title @blob.path, @ref -= render "header_title" = render 'projects/last_push' diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 57e507e68c8..87c732626a6 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -21,12 +21,10 @@ .controls.hidden-xs - if create_mr_button?(@repository.root_ref, branch.name) = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-grouped btn-xs' do - = icon('plus') Merge Request - if branch.name != @repository.root_ref = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-xs', method: :post, title: "Compare" do - = icon("exchange") Compare - if can_remove_branch?(@project, branch.name) diff --git a/app/views/projects/branches/destroy.js.haml b/app/views/projects/branches/destroy.js.haml deleted file mode 100644 index a21ddaf4930..00000000000 --- a/app/views/projects/branches/destroy.js.haml +++ /dev/null @@ -1 +0,0 @@ -$('.js-totalbranch-count').html("#{@repository.branch_count}") diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index ac7790421a4..e0367c40272 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -1,33 +1,34 @@ +- @no_container = true - page_title "Branches" -= render "projects/commits/header_title" = render "projects/commits/head" -.row-content-block - .pull-right + +%div{ class: (container_class) } + .top-area + .nav-text + Protected branches can be managed in project settings + - if can? current_user, :push_code, @project - = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do - = icon('plus') - New branch - - .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light - - if @sort.present? - = @sort.humanize - - else - Name - %b.caret - %ul.dropdown-menu.dropdown-menu-align-right - %li - = link_to namespace_project_branches_path(sort: nil) do - Name - = link_to namespace_project_branches_path(sort: 'recently_updated') do - = sort_title_recently_updated - = link_to namespace_project_branches_path(sort: 'last_updated') do - = sort_title_oldest_updated - .oneline - Protected branches can be managed in project settings -- unless @branches.empty? - %ul.content-list.all-branches - - @branches.each do |branch| - = render "projects/branches/branch", branch: branch - = paginate @branches, theme: 'gitlab' + .nav-controls + = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do + New branch + .dropdown.inline + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %span.light + - if @sort.present? + = @sort.humanize + - else + Name + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + %li + = link_to namespace_project_branches_path(sort: nil) do + Name + = link_to namespace_project_branches_path(sort: 'recently_updated') do + = sort_title_recently_updated + = link_to namespace_project_branches_path(sort: 'last_updated') do + = sort_title_oldest_updated + - unless @branches.empty? + %ul.content-list.all-branches + - @branches.each do |branch| + = render "projects/branches/branch", branch: branch + = paginate @branches, theme: 'gitlab' diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index c659af6338c..5a6c8c243fa 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -1,5 +1,4 @@ - page_title "New Branch" -= render "projects/commits/header_title" - if @error .alert.alert-danger diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml new file mode 100644 index 00000000000..51b5bd9db42 --- /dev/null +++ b/app/views/projects/builds/_header.html.haml @@ -0,0 +1,16 @@ +.content-block.build-header + = ci_status_with_icon(@build.status) + Build + %strong ##{@build.id} + for commit + = link_to ci_status_path(@build.pipeline) do + %strong= @build.pipeline.short_sha + from + = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do + %code + = @build.ref + - if @build.user + = render "user" + = time_ago_with_tooltip(@build.created_at) + %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } + = icon('angle-double-left') diff --git a/app/views/projects/builds/_header_title.html.haml b/app/views/projects/builds/_header_title.html.haml deleted file mode 100644 index 082dab1f5b0..00000000000 --- a/app/views/projects/builds/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Builds", project_builds_path(@project)) diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml new file mode 100644 index 00000000000..5d931389dfb --- /dev/null +++ b/app/views/projects/builds/_sidebar.html.haml @@ -0,0 +1,93 @@ +%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar + .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default + Build + %strong ##{@build.id} + %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" } + = icon('angle-double-right') + - if @build.coverage + .block.block-first + .title + Test coverage + %p.build-detail-row + #{@build.coverage}% + + - if can?(current_user, :read_build, @project) && @build.artifacts? + .block{ class: ("block-first" if !@build.coverage) } + .title + Build artifacts + .btn-group.btn-group-justified{ role: :group } + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do + Download + + - if @build.artifacts_metadata? + = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do + Browse + + .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && @build.artifacts?)) } + .title + Build details + - if @build.retryable? + = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post + - if @build.merge_request + %p.build-detail-row + %span.build-light-text Merge Request: + = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request) + - if @build.duration + %p.build-detail-row + %span.build-light-text Duration: + #{duration_in_words(@build.finished_at, @build.started_at)} + - if @build.finished_at + %p.build-detail-row + %span.build-light-text Finished: + #{time_ago_with_tooltip(@build.finished_at)} + - if @build.erased_at + %p.build-detail-row + %span.build-light-text Erased: + #{time_ago_with_tooltip(@build.erased_at)} + %p.build-detail-row + %span.build-light-text Runner: + - if @build.runner && current_user && current_user.admin + = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id) + - elsif @build.runner + \##{@build.runner.id} + .btn-group.btn-group-justified{ role: :group } + - if @build.has_trace? + = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' + - if @build.active? + = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post + - if can?(current_user, :update_build, @project) && @build.erasable? + = link_to erase_namespace_project_build_path(@project.namespace, @project, @build), + class: "btn btn-sm btn-default", method: :post, + data: { confirm: "Are you sure you want to erase this build?" } do + Erase + + - if @build.trigger_request + .build-widget + %h4.title + Trigger + + %p + %span.build-light-text Token: + #{@build.trigger_request.trigger.short_token} + + - if @build.trigger_request.variables + %p + %span.build-light-text Variables: + + %code + - @build.trigger_request.variables.each do |key, value| + #{key}=#{value} + + .block + .title + Commit message + %p.build-light-text.append-bottom-0 + #{@build.pipeline.git_commit_message} + + - if @build.tags.any? + .block + .title + Tags + - @build.tag_list.each do |tag| + %span.label.label-primary + = tag diff --git a/app/views/projects/builds/_user.html.haml b/app/views/projects/builds/_user.html.haml new file mode 100644 index 00000000000..2642de8021d --- /dev/null +++ b/app/views/projects/builds/_user.html.haml @@ -0,0 +1,4 @@ +by +%a{ href: user_path(@build.user) } + = image_tag avatar_icon(@build.user, 24), class: "avatar s24" + %strong= @build.user.to_reference diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 2e8015d119b..181547316aa 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -1,65 +1,63 @@ +- @no_container = true - page_title "Builds" -= render "header_title" - -.top-area - %ul.nav-links - %li{class: ('active' if @scope.nil?)} - = link_to project_builds_path(@project) do - All - %span.badge.js-totalbuilds-count - = number_with_delimiter(@all_builds.count(:id)) - - - %li{class: ('active' if @scope == 'running')} - = link_to project_builds_path(@project, scope: :running) do - Running - %span.badge.js-running-count - = number_with_delimiter(@all_builds.running_or_pending.count(:id)) - - %li{class: ('active' if @scope == 'finished')} - = link_to project_builds_path(@project, scope: :finished) do - Finished - %span.badge.js-running-count - = number_with_delimiter(@all_builds.finished.count(:id)) - - .nav-controls - - if can?(current_user, :update_build, @project) - - if @all_builds.running_or_pending.any? - = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), - data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - - - unless @repository.gitlab_ci_yml - = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' - - = link_to ci_lint_path, class: 'btn btn-default' do - = icon('wrench') - %span CI Lint - -.row-content-block - #{(@scope || 'running').capitalize} builds from this project - -%ul.content-list - - if @builds.blank? - %li - .nothing-here-block No builds to show - - else - .table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Commit - %th Ref - %th Stage - %th Name - %th Tags - %th Duration - %th Finished at - - if @project.build_coverage_enabled? - %th Coverage - %th - - = render @builds, commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled? - - = paginate @builds, theme: 'gitlab' += render "projects/pipelines/head" + +%div{ class: (container_class) } + .top-area + %ul.nav-links + %li{class: ('active' if @scope.nil?)} + = link_to project_builds_path(@project) do + All + %span.badge.js-totalbuilds-count + = number_with_delimiter(@all_builds.count(:id)) + + + %li{class: ('active' if @scope == 'running')} + = link_to project_builds_path(@project, scope: :running) do + Running + %span.badge.js-running-count + = number_with_delimiter(@all_builds.running_or_pending.count(:id)) + + %li{class: ('active' if @scope == 'finished')} + = link_to project_builds_path(@project, scope: :finished) do + Finished + %span.badge.js-running-count + = number_with_delimiter(@all_builds.finished.count(:id)) + + .nav-controls + - if can?(current_user, :update_build, @project) + - if @all_builds.running_or_pending.any? + = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), + data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post + + - unless @repository.gitlab_ci_yml + = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' + + = link_to ci_lint_path, class: 'btn btn-default' do + %span CI Lint + + %ul.content-list + - if @builds.blank? + %li + .nothing-here-block No builds to show + - else + .table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Commit + %th Ref + %th Stage + %th Name + %th Tags + %th Duration + %th Finished at + - if @project.build_coverage_enabled? + %th Coverage + %th + + = render @builds, commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: @project.build_coverage_enabled? + + = paginate @builds, theme: 'gitlab' diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index c0f7a7686f0..a26f8aeb315 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,19 +1,11 @@ - page_title "#{@build.name} (##{@build.id})", "Builds" -= render "header_title" +- trace_with_state = @build.trace_with_state +- header_title project_title(@project, "Builds", project_builds_path(@project)) .build-page - .row-content-block.top-block - Build ##{@build.id} for commit - %strong.monospace= link_to @build.commit.short_sha, ci_status_path(@build.commit) - from - = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref) - - merge_request = @build.merge_request - - if merge_request - via - = link_to "merge request #{merge_request.to_reference}", merge_request_path(merge_request) + = render "header" - #up-build-trace - - builds = @build.commit.builds.latest.to_a + - builds = @build.pipeline.builds.latest.to_a - if builds.size > 1 %ul.nav-links.no-top.no-bottom - builds.each do |build| @@ -33,18 +25,6 @@ · %i.fa.fa-warning This build was retried. - - .row-content-block.middle-block - .build-head - .clearfix - = ci_status_with_icon(@build.status) - - if @build.duration - %span - %i.fa.fa-time - #{duration_in_words(@build.finished_at, @build.started_at)} - .pull-right - #{time_ago_with_tooltip(@build.finished_at) if @build.finished_at} - - if @build.stuck? - unless @build.any_runners_online? .bs-callout.bs-callout-warning @@ -64,156 +44,27 @@ = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do Runners page - .row.prepend-top-default - .col-md-9 - .clearfix - - if @build.active? - .autoscroll-container - %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll - .clearfix + .prepend-top-default + - if @build.active? + .autoscroll-container + %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll #js-build-scroll.scroll-controls - = link_to '#up-build-trace', class: 'btn' do + = link_to '#build-trace', class: 'btn' do %i.fa.fa-angle-up = link_to '#down-build-trace', class: 'btn' do %i.fa.fa-angle-down + - if @build.erased? + .erased.alert.alert-warning + - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by + Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)} + - else + %pre.build-trace#build-trace + %code.bash.js-build-output + = icon("refresh spin", class: "js-build-refresh") - - if @build.erased? - .erased.alert.alert-warning - - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by - Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)} - - else - %pre.trace#build-trace - %code.bash - = preserve do - = raw @build.trace_html - - %div#down-build-trace - - .col-md-3 - - if @build.coverage - .build-widget - %h4.title - Test coverage - %h1 #{@build.coverage}% - - - if can?(current_user, :read_build, @project) && @build.artifacts? - .build-widget.artifacts - %h4.title Build artifacts - .center - .btn-group{ role: :group } - = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do - = icon('download') - Download - - - if @build.artifacts_metadata? - = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary' do - = icon('folder-open') - Browse - - .build-widget.build-controls - %h4.title - Build ##{@build.id} - - if can?(current_user, :update_build, @project) - .center - .btn-group{ role: :group } - - if @build.active? - = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-danger', method: :post - - elsif @build.retryable? - = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary', method: :post - - - if @build.erasable? - = link_to erase_namespace_project_build_path(@project.namespace, @project, @build), - class: 'btn btn-sm btn-warning', method: :post, - data: { confirm: 'Are you sure you want to erase this build?' } do - = icon('eraser') - Erase - - if @build.has_trace? - = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), - class: 'btn btn-sm btn-success', target: '_blank' - - .clearfix - - if @build.duration - %p - %span.attr-name Duration: - #{duration_in_words(@build.finished_at, @build.started_at)} - %p - %span.attr-name Created: - #{time_ago_with_tooltip(@build.created_at)} - - if @build.finished_at - %p - %span.attr-name Finished: - #{time_ago_with_tooltip(@build.finished_at)} - - if @build.erased_at - %p - %span.attr-name Erased: - #{time_ago_with_tooltip(@build.erased_at)} - %p - %span.attr-name Runner: - - if @build.runner && current_user && current_user.admin - = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id) - - elsif @build.runner - \##{@build.runner.id} - - - if @build.trigger_request - .build-widget - %h4.title - Trigger - - %p - %span.attr-name Token: - #{@build.trigger_request.trigger.short_token} - - - if @build.trigger_request.variables - %p - %span.attr-name Variables: - - %code - - @build.trigger_request.variables.each do |key, value| - #{key}=#{value} - - .build-widget - %h4.title - Commit - .pull-right - %small - = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace" - %p - %span.attr-name Branch: - = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref) - %p - %span.attr-name Author: - #{@build.commit.git_author_name} - %p - %span.attr-name Message: - #{@build.commit.git_commit_message} - - - if @build.tags.any? - .build-widget - %h4.title - Tags - - @build.tag_list.each do |tag| - %span.label.label-primary - = tag - - - if @builds.present? - .build-widget - %h4.title #{pluralize(@builds.count(:id), "other build")} for - = succeed ":" do - = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace" - %table.table.builds - - @builds.each_with_index do |build, i| - %tr.build - %td - = ci_icon_for_status(build.status) - %td - = link_to namespace_project_build_path(@project.namespace, @project, build) do - - if build.name - = build.name - - else - %span ##{build.id} - - %td.status= build.status + #down-build-trace += render "sidebar" - :javascript - new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}") +:javascript + new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}", "#{trace_with_state[:state]}") diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 1e4c46fca2f..16b8e1cca91 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -2,7 +2,7 @@ .btn-group %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} = icon('plus') - %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown + %ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown - can_create_issue = can?(current_user, :create_issue, @project) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - can_create_snippet = can?(current_user, :create_snippet, @project) diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 5fb5fe5af2f..34ad9fe2c43 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -12,7 +12,8 @@ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do = icon('code-fork fw') Fork - = link_to namespace_project_forks_path(@project.namespace, @project), class: 'count-with-arrow' do + %div.count-with-arrow %span.arrow %span.count - = @project.forks_count + = link_to namespace_project_forks_path(@project.namespace, @project) do + = @project.forks_count diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml index c1e3e5b73a2..3b97dc9328f 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/projects/buttons/_notifications.html.haml @@ -1,11 +1,11 @@ - if @notification_setting = form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f| = f.hidden_field :level - %span.dropdown - %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"} + .dropdown + %button.btn.btn-default.notifications-btn#notifications-button{ data: { toggle: "dropdown" }, aria: { haspopup: "true", expanded: "false" } } = icon('bell') = notification_title(@notification_setting.level) - = icon('angle-down') - %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-align-right.dropdown-menu-selectable.dropdown-menu-large{ role: "menu" } - NotificationSetting.levels.each do |level| = notification_list_item(level.first, @notification_setting) diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 8e95f040273..5bd6e3f0ebc 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -13,7 +13,9 @@ %strong ##{build.id} - if build.stuck? - %i.fa.fa-warning.text-warning + = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') + - if defined?(retried) && retried + = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') - if defined?(commit_sha) && commit_sha %td @@ -70,11 +72,11 @@ .pull-right - if can?(current_user, :read_build, build) && build.artifacts? = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do - %i.fa.fa-download + = icon('download') - if can?(current_user, :update_build, build) - if build.active? = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do - %i.fa.fa-remove.cred + = icon('remove', class: 'cred') - elsif defined?(allow_retry) && allow_retry && build.retryable? = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do - %i.fa.fa-refresh + = icon('refresh') diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml new file mode 100644 index 00000000000..a0ffa065067 --- /dev/null +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -0,0 +1,71 @@ +- status = pipeline.status +%tr.commit + %td.commit-link + = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: "ci-status ci-#{status}" do + = ci_icon_for_status(status) + %strong ##{pipeline.id} + + %td + %div.branch-commit + - if pipeline.ref + = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace" + · + = link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace" + + - if pipeline.tag? + %span.label.label-primary tag + - elsif pipeline.latest? + %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest + - if pipeline.triggered? + %span.label.label-primary triggered + - if pipeline.yaml_errors.present? + %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid + - if pipeline.builds.any?(&:stuck?) + %span.label.label-warning stuck + + %p.commit-title + - if commit_data = pipeline.commit_data + = link_to_gfm truncate(commit_data.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit_data.id), class: "commit-row-message" + - else + Cant find HEAD commit for this branch + + + - stages_status = pipeline.statuses.stages_status + - stages.each do |stage| + %td + - status = stages_status[stage] + - tooltip = "#{stage.titleize}: #{status || 'not found'}" + - if status + = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do + = ci_icon_for_status(status) + - else + .light.has-tooltip{ title: tooltip } + \- + + %td + - if pipeline.started_at && pipeline.finished_at + %p.duration + #{duration_in_words(pipeline.finished_at, pipeline.started_at)} + + %td + .controls.hidden-xs.pull-right + - artifacts = pipeline.builds.latest.select { |b| b.artifacts? } + - if artifacts.present? + .dropdown.inline.build-artifacts + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + = icon('download') + %b.caret + %ul.dropdown-menu.dropdown-menu-align-right + - artifacts.each do |build| + %li + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do + = icon("download") + %span #{build.name} + + - if can?(current_user, :update_pipeline, @project) + - if pipeline.retryable? + = link_to retry_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do + = icon("repeat") + - if pipeline.cancelable? + = link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do + = icon("remove") diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml index 5c9a319edeb..a508382578a 100644 --- a/app/views/projects/commit/_builds.html.haml +++ b/app/views/projects/commit/_builds.html.haml @@ -1,2 +1,2 @@ -- @ci_commits.each do |ci_commit| - = render "ci_commit", ci_commit: ci_commit +- @pipelines.each do |pipeline| + = render "pipeline", pipeline: pipeline, pipeline_details: true diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml deleted file mode 100644 index e849aefb188..00000000000 --- a/app/views/projects/commit/_ci_commit.html.haml +++ /dev/null @@ -1,71 +0,0 @@ -.row-content-block.build-content.middle-block - .pull-right - - if can?(current_user, :update_build, @project) - - if ci_commit.builds.latest.failed.any?(&:retryable?) - = link_to "Retry failed", retry_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post - - - if ci_commit.builds.running_or_pending.any? - = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post - - .oneline - = pluralize ci_commit.statuses.count(:id), "build" - - if ci_commit.ref - for - %span.label.label-info - = ci_commit.ref - - if defined?(link_to_commit) && link_to_commit - for commit - = link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace" - - if ci_commit.duration - in - = time_interval_in_words ci_commit.duration - -- if ci_commit.yaml_errors.present? - .bs-callout.bs-callout-danger - %h4 Found errors in your .gitlab-ci.yml: - %ul - - ci_commit.yaml_errors.split(",").each do |error| - %li= error - You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} - -- if @project.builds_enabled? && !ci_commit.ci_yaml_file - .bs-callout.bs-callout-warning - \.gitlab-ci.yml not found in this commit - -.table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Stage - %th Name - %th Tags - %th Duration - %th Finished at - - if @project.build_coverage_enabled? - %th Coverage - %th - - builds = ci_commit.statuses.latest.ordered - = render builds, coverage: @project.build_coverage_enabled?, stage: true, ref: false, allow_retry: true - -- if ci_commit.retried.any? - .row-content-block.second-block - Retried builds - - .table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Ref - %th Stage - %th Name - %th Tags - %th Duration - %th Finished at - - if @project.build_coverage_enabled? - %th Coverage - %th - = render ci_commit.retried, coverage: @project.build_coverage_enabled?, stage: true, ref: false diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml new file mode 100644 index 00000000000..ae7bb01223e --- /dev/null +++ b/app/views/projects/commit/_ci_stage.html.haml @@ -0,0 +1,15 @@ +%tr + %th{colspan: 10} + %strong + %a{name: stage} + - status = statuses.latest.status + %span{class: "ci-status-link ci-status-icon-#{status}"} + = ci_icon_for_status(status) + - if stage + + = stage.titleize.pluralize + = render statuses.latest.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true + = render statuses.retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true + %tr + %td{colspan: 10} + diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 01163e526b2..b117517c0dd 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,37 +1,35 @@ -.pull-right.commit-action-buttons - %div - - if @notes_count > 0 - %span.btn.disabled.btn-grouped - %i.fa.fa-comment +.commit-info-row.commit-info-row-header + %span.hidden-xs Authored by + %strong + = commit_author_link(@commit, avatar: true, size: 24) + #{time_ago_with_tooltip(@commit.authored_date)} + + .pull-right.commit-action-buttons + - if defined?(@notes_count) && @notes_count > 0 + %span.btn.disabled.btn-grouped.hidden-xs + = icon('comment') = @notes_count - .pull-left.btn-group - %a.btn.btn-grouped.dropdown-toggle{ data: {toggle: :dropdown} } - %i.fa.fa-download - Download as - %span.caret - %ul.dropdown-menu + = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-grouped hidden-xs hidden-sm" do + Browse Files + .dropdown.inline + %a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } } + %span.hidden-xs Options + %span.caret.commit-options-dropdown-caret + %ul.dropdown-menu.dropdown-menu-align-right + %li.visible-xs-block.visible-sm-block + = link_to namespace_project_tree_path(@project.namespace, @project, @commit) do + Browse Files + - unless @commit.has_been_reverted?(current_user) + %li.clearfix + = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) + %li.clearfix + = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) + %li.divider + %li.dropdown-header + Download - unless @commit.parents.length > 1 %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch) %li= link_to "Plain Diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff) - = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-grouped" do - = icon('files-o') - Browse Files - - unless @commit.has_been_reverted?(current_user) - = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id)) - = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id)) - %div - -%p -.commit-info-row - - if @commit.status - = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status ci-#{@commit.status}" do - = ci_icon_for_status(@commit.status) - build: - = ci_label_for_status(@commit.status) - %span.light Authored by - %strong - = commit_author_link(@commit, avatar: true, size: 24) - #{time_ago_with_tooltip(@commit.authored_date)} - if @commit.different_committer? .commit-info-row @@ -41,8 +39,9 @@ #{time_ago_with_tooltip(@commit.committed_date)} .commit-info-row - %span.light Commit - = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" + %span.hidden-xs.hidden-sm Commit + = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace hidden-xs hidden-sm" + = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace visible-xs-inline visible-sm-inline" = clipboard_button(clipboard_text: @commit.id) %span.cgray= pluralize(@commit.parents.count, "parent") - @commit.parents.each do |parent| @@ -51,12 +50,23 @@ %span.commit-info.branches %i.fa.fa-spinner.fa-spin +- if @commit.status + .commit-info-row + Builds for + = pluralize(@commit.pipelines.count, 'pipeline') + = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status-link ci-status-icon-#{@commit.status}" do + = ci_icon_for_status(@commit.status) + = ci_label_for_status(@commit.status) + - if @commit.pipelines.duration + in + = time_interval_in_words @commit.pipelines.duration + .commit-box.content-block %h3.commit-title - = markdown escape_once(@commit.title), pipeline: :single_line + = markdown escape_once(@commit.title), pipeline: :single_line, author: @commit.author - if @commit.description.present? %pre.commit-description - = preserve(markdown(escape_once(@commit.description), pipeline: :single_line)) + = preserve(markdown(escape_once(@commit.description), pipeline: :single_line, author: @commit.author)) :javascript $(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}"); diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml new file mode 100644 index 00000000000..0411137b7c6 --- /dev/null +++ b/app/views/projects/commit/_pipeline.html.haml @@ -0,0 +1,52 @@ +.row-content-block.build-content.middle-block + .pull-right + - if can?(current_user, :update_pipeline, pipeline.project) + - if pipeline.builds.latest.failed.any?(&:retryable?) + = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post + + - if pipeline.builds.running_or_pending.any? + = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post + + .oneline.clearfix + - if defined?(pipeline_details) && pipeline_details + Pipeline + = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace" + with + = pluralize pipeline.statuses.count(:id), "build" + - if pipeline.ref + for + = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace" + - if defined?(link_to_commit) && link_to_commit + for commit + = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace" + - if pipeline.duration + in + = time_interval_in_words pipeline.duration + +- if pipeline.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - pipeline.yaml_errors.split(",").each do |error| + %li= error + You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} + +- if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file + .bs-callout.bs-callout-warning + \.gitlab-ci.yml not found in this commit + +.table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Name + %th Tags + %th Duration + %th Finished at + - if pipeline.project.build_coverage_enabled? + %th Coverage + %th + - pipeline.statuses.stages.each do |stage| + = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.where(stage: stage) diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml index 7118a4846c6..2f051fb90e0 100644 --- a/app/views/projects/commit/builds.html.haml +++ b/app/views/projects/commit/builds.html.haml @@ -1,7 +1,7 @@ - page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits" -= render "projects/commits/header_title" + .prepend-top-default = render "commit_box" -= render "ci_menu" += render "ci_menu" = render "builds" diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index e5e3d696035..401cb4f7e30 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -1,8 +1,6 @@ - page_title "#{@commit.title} (#{@commit.short_id})", "Commits" - page_description @commit.description -= render "projects/commits/header_title" - .prepend-top-default = render "commit_box" - if @commit.status diff --git a/app/views/projects/commits/_commit.atom.builder b/app/views/projects/commits/_commit.atom.builder new file mode 100644 index 00000000000..1657fb46163 --- /dev/null +++ b/app/views/projects/commits/_commit.atom.builder @@ -0,0 +1,14 @@ +xml.entry do + xml.id namespace_project_commit_url(@project.namespace, @project, id: commit.id) + xml.link href: namespace_project_commit_url(@project.namespace, @project, id: commit.id) + xml.title truncate(commit.title, length: 80) + xml.updated commit.committed_date.xmlschema + xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(commit.author_email)) + + xml.author do |author| + xml.name commit.author_name + xml.email commit.author_email + end + + xml.summary markdown(commit.description, pipeline: :single_line) +end diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index c7d8c9a0d15..367027182b6 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -17,14 +17,14 @@ .pull-right - if commit.status - = render_ci_status(commit) + = render_commit_status(commit) = clipboard_button(clipboard_text: commit.id) = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" - if commit.description? .commit-row-description.js-toggle-content %pre - = preserve(markdown(escape_once(commit.description), pipeline: :single_line)) + = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author)) .commit-row-info by diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 64e8da9201d..7283a78a64e 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -3,7 +3,7 @@ - commits, hidden = limited_commits(@commits) -- commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits| +- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| .row.commits-row .col-md-2.hidden-xs.hidden-sm %h5.commits-row-date diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index d1bd76ab529..a72e8ba73ad 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,24 +1,28 @@ -%ul.nav-links - = nav_link(controller: [:commit, :commits]) do - = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do - Commits - %span.badge - = number_with_delimiter(@repository.commit_count) +.scrolling-tabs-container + %ul.nav-links.sub-nav.scrolling-tabs + %div{ class: (container_class) } + .fade-left + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_files_path(@project) do + Files - = nav_link(controller: %w(network)) do - = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do - Network + = nav_link(controller: [:commit, :commits]) do + = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do + Commits - = nav_link(controller: :compare) do - = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do - Compare + = nav_link(controller: %w(network)) do + = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do + Network - = nav_link(html_options: {class: branches_tab_class}) do - = link_to namespace_project_branches_path(@project.namespace, @project) do - Branches - %span.badge.js-totalbranch-count= @repository.branch_count + = nav_link(controller: :compare) do + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do + Compare - = nav_link(controller: [:tags, :releases]) do - = link_to namespace_project_tags_path(@project.namespace, @project) do - Tags - %span.badge.js-totaltags-count= @repository.tag_count + = nav_link(html_options: {class: branches_tab_class}) do + = link_to namespace_project_branches_path(@project.namespace, @project) do + Branches + + = nav_link(controller: [:tags, :releases]) do + = link_to namespace_project_tags_path(@project.namespace, @project) do + Tags + .fade-right diff --git a/app/views/projects/commits/_header_title.html.haml b/app/views/projects/commits/_header_title.html.haml deleted file mode 100644 index e4385893dd9..00000000000 --- a/app/views/projects/commits/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Commits", project_commits_path(@project)) diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index e310fafd82c..30bb7412073 100644 --- a/app/views/projects/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder @@ -6,18 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id namespace_project_commits_url(@project.namespace, @project, @ref) xml.updated @commits.first.committed_date.xmlschema if @commits.any? - @commits.each do |commit| - xml.entry do - xml.id namespace_project_commit_url(@project.namespace, @project, id: commit.id) - xml.link href: namespace_project_commit_url(@project.namespace, @project, id: commit.id) - xml.title truncate(commit.title, length: 80) - xml.updated commit.committed_date.xmlschema - xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(commit.author_email)) - xml.author do |author| - xml.name commit.author_name - xml.email commit.author_email - end - xml.summary markdown(commit.description, pipeline: :single_line) - end - end + xml << render(@commits) if @commits.any? end diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 088eaa28013..76ba0bea36d 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -1,42 +1,44 @@ +- @no_container = true + - page_title "Commits", @ref -= render "header_title" = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") = render "head" -.row-content-block.second-block - .tree-ref-holder - = render 'shared/ref_switcher', destination: 'commits' +%div{ class: (container_class) } + .row-content-block.second-block.content-component-block + .tree-ref-holder + = render 'shared/ref_switcher', destination: 'commits' + + .block-controls.hidden-xs.hidden-sm + - if @merge_request.present? + .control + = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' + - elsif create_mr_button?(@repository.root_ref, @ref) + .control + = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do + = icon('plus') + Create Merge Request - .block-controls.hidden-xs.hidden-sm - - if @merge_request.present? - .control - = link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn' - - elsif create_mr_button?(@repository.root_ref, @ref) .control - = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do - = icon('plus') - Create Merge Request + = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'pull-left commits-search-form') do + = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input', spellcheck: false } - .control - = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'pull-left commits-search-form') do - = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input', spellcheck: false } - - - if current_user && current_user.private_token - .control - = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do - = icon("rss") + - if current_user && current_user.private_token + .control + = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do + = icon("rss") - %ul.breadcrumb.repo-breadcrumb - = commits_breadcrumbs + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs -%div{id: dom_id(@project)} - #commits-list.content_list= render "commits", project: @project -.clear -= spinner + %div{id: dom_id(@project)} + #commits-list.content_list= render "commits", project: @project + .clear + = spinner :javascript CommitsList.init(#{@limit}); diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index 5e188dd0f3c..c322942aeba 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -1,17 +1,18 @@ +- @no_container = true - page_title "Compare" -= render "projects/commits/header_title" = render "projects/commits/head" -.row-content-block - Compare branches, tags or commit ranges. - %br - Fill input field with commit id like - %code.label-branch 4eedf23 - or branch/tag name like - %code.label-branch master - and press compare button for the commits list and a code diff. - %br - Changes are shown <b>from</b> the version in the first field <b>to</b> the version in the second field. +%div{ class: (container_class) } + .row-content-block.second-block.content-component-block + Compare branches, tags or commit ranges. + %br + Fill input field with commit id like + %code.label-branch 4eedf23 + or branch/tag name like + %code.label-branch master + and press compare button for the commits list and a code diff. + %br + Changes are shown <b>from</b> the version in the first field <b>to</b> the version in the second field. -.prepend-top-20 - = render "form" + .prepend-top-20 + = render "form" diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 62525168239..cdc34f51d6d 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -1,5 +1,4 @@ - page_title "#{params[:from]}...#{params[:to]}" -= render "projects/commits/header_title" = render "projects/commits/head" diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml new file mode 100644 index 00000000000..4e9f936539b --- /dev/null +++ b/app/views/projects/container_registry/_tag.html.haml @@ -0,0 +1,21 @@ +%tr.tag + %td + = escape_once(tag.name) + = clipboard_button(clipboard_text: "docker pull #{tag.path}") + %td + - if layer = tag.layers.first + %span.has-tooltip{ title: "#{layer.revision}" } + = layer.short_revision + - else + \- + %td + = number_to_human_size(tag.total_size) + · + = pluralize(tag.layers.size, "layer") + %td + = time_ago_in_words(tag.created_at) + - if can?(current_user, :update_container_image, @project) + %td.content + .controls.hidden-xs.pull-right + = link_to namespace_project_container_registry_path(@project.namespace, @project, tag.name), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do + = icon("trash cred") diff --git a/app/views/projects/container_registry/index.html.haml b/app/views/projects/container_registry/index.html.haml new file mode 100644 index 00000000000..993da27310f --- /dev/null +++ b/app/views/projects/container_registry/index.html.haml @@ -0,0 +1,39 @@ +- page_title "Container Registry" + +%hr + +%ul.content-list + %li.light.prepend-top-default + %p + A 'container image' is a snapshot of a container. + You can host your container images with GitLab. + %br + To start using container images hosted on GitLab you first need to login: + %pre + %code + docker login #{Gitlab.config.registry.host_port} + %br + Then you are free to create and upload a container image with build and push commands: + %pre + docker build -t #{escape_once(@project.container_registry_repository_url)} . + %br + docker push #{escape_once(@project.container_registry_repository_url)} + + - if @tags.blank? + %li + .nothing-here-block No images in Container Registry for this project. + + - else + .table-holder + %table.table.tags + %thead + %tr + %th Name + %th Image ID + %th Size + %th Created + - if can?(current_user, :update_container_image, @project) + %th + + - @tags.each do |tag| + = render 'tag', tag: tag diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml index e230834e8ba..04fbb37d93f 100644 --- a/app/views/projects/deploy_keys/index.html.haml +++ b/app/views/projects/deploy_keys/index.html.haml @@ -19,7 +19,7 @@ %ul.well-list = render @enabled_keys - else - .profile-settings-message.text-center + .settings-message.text-center No deploy keys found. Create one with the form above or add existing one below. %h5.prepend-top-default Deploy keys from projects you have access to (#{@available_project_keys.size}) @@ -27,7 +27,7 @@ %ul.well-list = render @available_project_keys - else - .profile-settings-message.text-center + .settings-message.text-center No deploy keys from your projects could be found. Create one with the form above or add existing one below. - if @available_public_keys.any? %h5.prepend-top-default diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 0f04fc5d33c..e5983c58039 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -11,11 +11,9 @@ = link_to "#diff-#{i}" do - if diff_file.renamed_file - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) - .filename.old - = old_path + = old_path → - .filename.new - = new_path + = new_path - else %span = diff_file.new_path @@ -41,7 +39,7 @@ .diff-content.diff-wrap-lines - # Skip all non non-supported blobs - - return unless blob.respond_to?('text?') + - return unless blob.respond_to?(:text?) - if diff_file.too_large? .nothing-here-block This diff could not be displayed because it is too large. - elsif blob_text_viewable?(blob) && !project.repository.diffable?(blob) diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 107097ad963..f1577e8a47b 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -15,7 +15,7 @@ = link_text - else = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - - if @comments_allowed && can?(current_user, :create_note, @project) + - if !@diff_notes_disabled && can?(current_user, :create_note, @project) = link_to_new_diff_note(line_code) %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } - link_text = type == "old" ? " ".html_safe : line.new_pos diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 81948513e43..4ecc9528bd2 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -16,7 +16,7 @@ - else %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]} #{'empty-cell' if !left[:number]}"} = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] - - if @comments_allowed && can?(current_user, :create_note, @project) + - if !@diff_notes_disabled && can?(current_user, :create_note, @project) = link_to_new_diff_note(left[:line_code], 'old') %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]} #{'empty-cell' if left[:text].empty?}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) @@ -29,14 +29,14 @@ %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }} = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - - if @comments_allowed && can?(current_user, :create_note, @project) - = link_to_new_diff_note(right[:line_code], 'new') + - if !@diff_notes_disabled && can?(current_user, :create_note, @project) + = link_to_new_diff_note(new_line_code, 'new') %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) - - if @reply_allowed - - comments_left, comments_right = organize_comments(left[:type], right[:type], left[:line_code], right[:line_code]) - - if comments_left.present? || comments_right.present? - = render "projects/notes/diff_notes_with_reply_parallel", notes_left: comments_left, notes_right: comments_right + - unless @diff_notes_disabled + - notes_left, notes_right = organize_comments(left, right) + - if notes_left.present? || notes_right.present? + = render "projects/notes/diff_notes_with_reply_parallel", notes_left: notes_left, notes_right: notes_right - if diff_file.diff.diff.blank? && diff_file.mode_changed? .file-mode-changed diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index e7169d7b599..068593a7dd1 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -6,16 +6,15 @@ %table.text-file.code.js-syntax-highlight{ class: too_big ? 'hide' : '' } - last_line = 0 - - raw_diff_lines = diff_file.diff_lines.to_a - diff_file.highlighted_diff_lines.each_with_index do |line, index| - line_code = generate_line_code(diff_file.file_path, line) - last_line = line.new_pos = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: line_code} - - if @reply_allowed - - comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at) - - unless comments.empty? - = render "projects/notes/diff_notes_with_reply", notes: comments, line: raw_diff_lines[index].text + - unless @diff_notes_disabled + - diff_notes = @grouped_diff_notes[line_code] + - if diff_notes + = render "projects/notes/diff_notes_with_reply", notes: diff_notes - if last_line > 0 = render "projects/diffs/match_line", { line: "", diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 76a4f41193c..8449fe1e4e0 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -1,251 +1,223 @@ -.project-edit-container.prepend-top-default - .project-edit-errors - .project-edit-content - .panel.panel-default - .panel-heading +.project-edit-container + .row.prepend-top-default + .col-lg-3.profile-settings-sidebar + %h4.prepend-top-0 Project settings - .panel-body - = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit_project form-horizontal fieldset-form" }, authenticity_token: true do |f| - - %fieldset - .form-group.project_name_holder - = f.label :name, class: 'control-label' do - Project name - .col-sm-10 - = f.text_field :name, class: "form-control", id: "project_name_edit" - - - .form-group - = f.label :description, class: 'control-label' do - Project description - %span.light (optional) - .col-sm-10 - = f.text_area :description, class: "form-control", rows: 3, maxlength: 250 - - - unless @project.empty_repo? - .form-group - = f.label :default_branch, "Default Branch", class: 'control-label' - .col-sm-10= f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) - - - = render 'shared/visibility_level', f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can_change_visibility_level?(@project, current_user), form_model: @project - + .col-lg-9 + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f| + %fieldset.append-bottom-0 .form-group - = f.label :tag_list, "Tags", class: 'control-label' - .col-sm-10 - = f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control" - %p.help-block Separate tags with commas. - - %fieldset.features - %legend - Features: - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :issues_enabled do - = f.check_box :issues_enabled - %strong Issues - %br - %span.descr Lightweight issue tracking system for this project - - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :merge_requests_enabled do - = f.check_box :merge_requests_enabled - %strong Merge Requests - %br - %span.descr Submit changes to be merged upstream - - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :builds_enabled do - = f.check_box :builds_enabled - %strong Builds - %br - %span.descr Test and deploy your changes before merge - - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :wiki_enabled do - = f.check_box :wiki_enabled - %strong Wiki - %br - %span.descr Pages for project documentation - - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :snippets_enabled do - = f.check_box :snippets_enabled - %strong Snippets - %br - %span.descr Share code pastes with others out of git repository - - = render 'builds_settings', f: f + = f.label :name, class: 'label-light' do + Project name + = f.text_field :name, class: "form-control", id: "project_name_edit" + .form-group + = f.label :description, class: 'label-light' do + Project description + %span.light (optional) + = f.text_area :description, class: "form-control", rows: 3, maxlength: 250 - %fieldset.features - %legend - Project avatar: + - unless @project.empty_repo? .form-group - .col-sm-offset-2.col-sm-10 - - if @project.avatar? - = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160') - %p.light - - if @project.avatar_in_git - Project avatar in repository: #{ @project.avatar_in_git } - %p.light - - if @project.avatar? - You can change your project avatar here - - else - You can upload a project avatar here - %a.choose-btn.btn.btn-sm.js-choose-project-avatar-button - %i.icon-paper-clip - %span Choose File ... - - %span.file_name.js-avatar-filename File name... - = f.file_field :avatar, class: "js-project-avatar-input hidden" - .light The maximum file size allowed is 200KB. - - if @project.avatar? - %hr - = link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" - - - .form-actions - = f.submit 'Save changes', class: "btn btn-save" - - - - .danger-settings - .panel.panel-default - .panel-heading Housekeeping - .errors-holder - .panel-body - %p - Runs a number of housekeeping tasks within the current repository, - such as compressing file revisions and removing unreachable objects. - %br - - .form-actions - = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), - method: :post, class: "btn btn-default" - - - if can? current_user, :archive_project, @project - - if @project.archived? - .panel.panel-success - .panel-heading - Unarchive project - .panel-body - %p - Unarchiving the project will mark its repository as active. + = f.label :default_branch, "Default Branch", class: 'label-light' + = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) + .form-group.project-visibility-level-holder + = f.label :visibility_level, class: 'label-light' do + Visibility Level + = link_to "(?)", help_page_path("public_access", "public_access") + - if can_change_visibility_level?(@project, current_user) + = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: @project.visibility_level, form_model: @project) + - else + .info + = visibility_level_icon(@project.visibility_level) + %strong + = visibility_level_label(@project.visibility_level) + .light= visibility_level_description(@project.visibility_level, @project) + .form-group + = f.label :tag_list, "Tags", class: 'label-light' + = f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control" + %p.help-block Separate tags with commas. + %hr + %fieldset.features.append-bottom-0 + %h5.prepend-top-0 + Features + .form-group + .checkbox + = f.label :issues_enabled do + = f.check_box :issues_enabled + %strong Issues %br - The project can be committed to. + %span.descr Lightweight issue tracking system for this project + .form-group + .checkbox + = f.label :merge_requests_enabled do + = f.check_box :merge_requests_enabled + %strong Merge Requests %br - %strong Once active this project shows up in the search and on the dashboard. - - .form-actions - = link_to 'Unarchive project', unarchive_namespace_project_path(@project.namespace, @project), - data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, - method: :post, class: "btn btn-success" - - else - .panel.panel-warning - .panel-heading - Archive project - .panel-body - %p - Archiving the project will mark its repository as read-only. + %span.descr Submit changes to be merged upstream + .form-group + .checkbox + = f.label :builds_enabled do + = f.check_box :builds_enabled + %strong Builds + %br + %span.descr Test and deploy your changes before merge + .form-group + .checkbox + = f.label :wiki_enabled do + = f.check_box :wiki_enabled + %strong Wiki %br - It is hidden from the dashboard and doesn't show up in searches. + %span.descr Pages for project documentation + .form-group + .checkbox + = f.label :snippets_enabled do + = f.check_box :snippets_enabled + %strong Snippets %br - %strong Archived projects cannot be committed to! - - .form-actions - = link_to 'Archive project', archive_namespace_project_path(@project.namespace, @project), - data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, - method: :post, class: "btn btn-warning" - - else - .nothing-here-block Only the project owner can archive a project - - .panel.panel-default.panel.panel-warning - .panel-heading Rename repository - .errors-holder - .panel-body - = form_for([@project.namespace.becomes(Namespace), @project], html: { class: 'form-horizontal' }) do |f| - .form-group.project_name_holder - = f.label :name, class: 'control-label' do - Project name - .col-sm-9 - .form-group - = f.text_field :name, class: "form-control" + %span.descr Share code pastes with others out of git repository + - if Gitlab.config.registry.enabled .form-group - = f.label :path, class: 'control-label' do - %span Path - .col-sm-9 - .form-group - .input-group - .input-group-addon - #{URI.join(root_url, @project.namespace.path)}/ - = f.text_field :path, class: 'form-control' - %ul - %li Be careful. Renaming a project's repository can have unintended side effects. - %li You will need to update your local repositories to point to the new location. - .form-actions - = f.submit 'Rename project', class: "btn btn-warning" - - - if can?(current_user, :change_namespace, @project) - .panel.panel-default.panel.panel-danger - .panel-heading Transfer project - .errors-holder - .panel-body - = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_namespace_project_path(@project.namespace, @project), method: :put, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f| - .form-group - = label_tag :new_namespace_id, nil, class: 'control-label' do - %span Namespace - .col-sm-9 - .form-group - = select_tag :new_namespace_id, namespaces_options(@project.namespace_id), { prompt: 'Choose a project namespace', class: 'select2' } - %ul - %li Be careful. Changing the project's namespace can have unintended side effects. - %li You can only transfer the project to namespaces you manage. - %li You will need to update your local repositories to point to the new location. - %li Project visibility level will be changed to match namespace rules when transfering to a group. - .form-actions - = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } - - else - .nothing-here-block Only the project owner can transfer a project - - - if @project.forked? - - if can?(current_user, :remove_fork_project, @project) - = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :delete, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f| - .panel.panel-default.panel.panel-danger - .panel-heading Remove fork relationship - .panel-body - %p - This will remove the fork relationship to source project - #{link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)}. + .checkbox + = f.label :container_registry_enabled do + = f.check_box :container_registry_enabled + %strong Container Registry %br - %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. - .form-actions - = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } + %span.descr Enable Container Registry for this repository + %hr + = render 'merge_request_settings', f: f + %hr + = render 'builds_settings', f: f + %hr + %fieldset.features.append-bottom-default + %h5.prepend-top-0 + Project avatar + .form-group + - if @project.avatar? + = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160') + %p.light + - if @project.avatar_in_git + Project avatar in repository: #{ @project.avatar_in_git } + %a.choose-btn.btn.js-choose-project-avatar-button + Browse file... + %span.file_name.prepend-left-default.js-avatar-filename No file chosen + = f.file_field :avatar, class: "js-project-avatar-input hidden" + .help-block The maximum file size allowed is 200KB. + - if @project.avatar? + %hr + = link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" + = f.submit 'Save changes', class: "btn btn-save" + .row.prepend-top-default + %hr + .row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0 + Housekeeping + %p.append-bottom-0 + %p + Runs a number of housekeeping tasks within the current repository, + such as compressing file revisions and removing unreachable objects. + .col-lg-9 + = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project), + method: :post, class: "btn btn-save" + %hr + - if can? current_user, :archive_project, @project + .row.prepend-top-default + .col-lg-3 + %h4.warning-title.prepend-top-0 + - if @project.archived? + Unarchive project + - else + Archive project + %p.append-bottom-0 + - if @project.archived? + Unarchiving the project will mark its repository as active. The project can be committed to. + - else + Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches. + .col-lg-9 + - if @project.archived? + %p + %strong Once active this project shows up in the search and on the dashboard. + = link_to 'Unarchive project', unarchive_namespace_project_path(@project.namespace, @project), + data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, + method: :post, class: "btn btn-success" - else - .nothing-here-block Only the project owner can remove the fork relationship. - - - if can?(current_user, :remove_project, @project) - .panel.panel-default.panel.panel-danger - .panel-heading Remove project - .panel-body - = form_tag(namespace_project_path(@project.namespace, @project), method: :delete, class: 'form-horizontal') do - %p - Removing the project will delete its repository and all related resources including issues, merge requests etc. - %br - %strong Removed projects cannot be restored! - .form-actions - = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } - - else - .nothing-here-block Only the project owner can remove a project. - + %p + %strong Archived projects cannot be committed to! + = link_to 'Archive project', archive_namespace_project_path(@project.namespace, @project), + data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, + method: :post, class: "btn btn-warning" + %hr + .row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0.warning-title + Rename repository + .col-lg-9 + = form_for([@project.namespace.becomes(Namespace), @project]) do |f| + .form-group.project_name_holder + = f.label :name, class: 'label-light' do + Project name + .form-group + = f.text_field :name, class: "form-control" + .form-group + = f.label :path, class: 'label-light' do + %span Path + .form-group + .input-group + .input-group-addon + #{URI.join(root_url, @project.namespace.path)}/ + = f.text_field :path, class: 'form-control' + %ul + %li Be careful. Renaming a project's repository can have unintended side effects. + %li You will need to update your local repositories to point to the new location. + = f.submit 'Rename project', class: "btn btn-warning" + - if can?(current_user, :change_namespace, @project) + %hr + .row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0.danger-title + Transfer project + .col-lg-9 + = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_namespace_project_path(@project.namespace, @project), method: :put, remote: true) do |f| + .form-group + = label_tag :new_namespace_id, nil, class: 'label-light' do + %span Namespace + .form-group + = select_tag :new_namespace_id, namespaces_options(@project.namespace_id), { prompt: 'Choose a project namespace', class: 'select2' } + %ul + %li Be careful. Changing the project's namespace can have unintended side effects. + %li You can only transfer the project to namespaces you manage. + %li You will need to update your local repositories to point to the new location. + %li Project visibility level will be changed to match namespace rules when transfering to a group. + = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) } + - if @project.forked? && can?(current_user, :remove_fork_project, @project) + %hr + .row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0.danger-title + Remove fork relationship + %p.append-bottom-0 + %p + This will remove the fork relationship to source project + = succeed "." do + = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) + .col-lg-9 + = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f| + %p + %strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source. + = button_to 'Remove fork relationship', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_message(@project) } + - if can?(current_user, :remove_project, @project) + %hr + .row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0.danger-title + Remove project + %p.append-bottom-0 + Removing the project will delete its repository and all related resources including issues, merge requests etc. + .col-lg-9 + = form_tag(namespace_project_path(@project.namespace, @project), method: :delete) do + %p + %strong Removed projects cannot be restored! + = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) } .save-project-loader.hide .center @@ -254,5 +226,4 @@ Saving project. %p Please wait a moment, this page will automatically refresh when ready. - = render 'shared/confirm_modal', phrase: @project.path diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 1a2e59752fe..636beb73ec2 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -15,10 +15,14 @@ If you already have files you can push them using command line instructions below. %p Otherwise you can start with adding a - = link_to "README", new_readme_path, class: 'underlined-link' + = succeed ',' do + = link_to "README", new_readme_path, class: 'underlined-link' + a + = succeed ',' do + = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link' or a - = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link' - file to this project. + = link_to '.gitignore', add_special_file_path(@project, file_name: '.gitignore'), class: 'underlined-link' + to this project. - if can?(current_user, :push_code, @project) %div{ class: container_class } diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index 1fe1d98bf13..9322c82904f 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -1,5 +1,4 @@ - page_title "Find File", @ref -- header_title project_title(@project, "Files", project_files_path(@project)) .file-finder-holder.tree-holder.clearfix .nav-block diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index f21c864e35c..5bc5c71283e 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -12,6 +12,9 @@ - else %strong ##{generic_commit_status.id} + - if defined?(retried) && retried + = icon('warning', class: 'text-warning has-tooltip', title: 'Status was retried.') + - if defined?(commit_sha) && commit_sha %td = link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace" @@ -37,11 +40,13 @@ %td = generic_commit_status.name - .pull-right - - if generic_commit_status.tags.any? - - generic_commit_status.tags.each do |tag| - %span.label.label-primary - = tag + %td + - if generic_commit_status.tags.any? + - generic_commit_status.tags.each do |tag| + %span.label.label-primary + = tag + - if defined?(retried) && retried + %span.label.label-warning retried %td.duration - if generic_commit_status.duration diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index 79a56647c53..8becaea246f 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -1,3 +1,4 @@ +- page_specific_javascripts asset_path("graphs/application.js") %ul.nav-links = nav_link(action: :show) do = link_to 'Contributors', namespace_project_graph_path diff --git a/app/views/projects/graphs/_header_title.html.haml b/app/views/projects/graphs/_header_title.html.haml deleted file mode 100644 index 1e2f61cd22b..00000000000 --- a/app/views/projects/graphs/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Graphs", namespace_project_graph_path(@project.namespace, @project, current_ref)) diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml index 9f05be9982b..19ccc125ea8 100644 --- a/app/views/projects/graphs/ci.html.haml +++ b/app/views/projects/graphs/ci.html.haml @@ -1,5 +1,4 @@ - page_title "Continuous Integration", "Graphs" -= render "header_title" = render 'head' .row-content-block.append-bottom-default .oneline diff --git a/app/views/projects/graphs/ci/_overall.haml b/app/views/projects/graphs/ci/_overall.haml index 4b12e5f2da1..edc4f7b079f 100644 --- a/app/views/projects/graphs/ci/_overall.haml +++ b/app/views/projects/graphs/ci/_overall.haml @@ -16,4 +16,4 @@ %li Commits covered: %strong - = @project.ci_commits.count(:all) + = @project.pipelines.count(:all) diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index da9f648cc9c..d9b2fb6c065 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -1,5 +1,4 @@ - page_title "Commits", "Graphs" -= render "header_title" = render 'head' .row-content-block.append-bottom-default diff --git a/app/views/projects/graphs/languages.html.haml b/app/views/projects/graphs/languages.html.haml index ebecab1dbfc..249c16f4709 100644 --- a/app/views/projects/graphs/languages.html.haml +++ b/app/views/projects/graphs/languages.html.haml @@ -1,5 +1,4 @@ - page_title "Languages", "Graphs" -= render "header_title" = render 'head' .row-content-block.append-bottom-default diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index ad4a932d391..33970e7b909 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -1,5 +1,4 @@ - page_title "Contributors", "Graphs" -= render "header_title" = render 'head' .row-content-block.append-bottom-default @@ -19,7 +18,7 @@ .header.clearfix %h3#date_header.page-title %p.light - Commits to #{@ref}, excluding merge commits. Limited by 6,000 commits + Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits. %input#brush_change{:type => "hidden"} .graphs #contributors-master diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/hooks/_project_hook.html.haml index 62eba5888a4..8151187d499 100644 --- a/app/views/projects/hooks/_project_hook.html.haml +++ b/app/views/projects/hooks/_project_hook.html.haml @@ -3,7 +3,7 @@ .col-md-8.col-lg-7 %strong.light-header= hook.url %div - - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger| + - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events wiki_page_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray.deploy-project-label= trigger.titleize .col-md-4.col-lg-5.text-right-lg.prepend-top-5 diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index 36c1d69f060..8faad351463 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -1,84 +1 @@ -- page_title "Webhooks" -.row.prepend-top-default - .col-lg-3.profile-settings-sidebar - %h4.prepend-top-0 - = page_title - %p - #{link_to "Webhooks", help_page_path("web_hooks", "web_hooks")} can be - used for binding events when something is happening within the project. - .col-lg-9.append-bottom-default - %h5.prepend-top-0 - Add new webhook - = form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hooks_path(@project.namespace, @project) do |f| - = form_errors(@hook) - - .form-group - = f.label :url, "URL", class: "label-light" - = f.text_field :url, class: "form-control", placeholder: "http://example.com/trigger-ci.json" - .form-group - = f.label :token, "Secret Token", class: 'label-light' - = f.text_field :token, class: "form-control", placeholder: '' - %p.help-block - Use this token to validate received payloads - .form-group - = f.label :url, "Trigger", class: "label-light" - %div - = f.check_box :push_events, class: "pull-left" - .prepend-left-20 - = f.label :push_events, class: "label-light append-bottom-0" do - Push events - %p.light - This url will be triggered by a push to the repository - %div - = f.check_box :tag_push_events, class: "pull-left" - .prepend-left-20 - = f.label :tag_push_events, class: "label-light append-bottom-0" do - Tag push events - %p.light - This url will be triggered when a new tag is pushed to the repository - %div - = f.check_box :note_events, class: "pull-left" - .prepend-left-20 - = f.label :note_events, class: "label-light append-bottom-0" do - Comments - %p.light - This url will be triggered when someone adds a comment - %div - = f.check_box :issues_events, class: "pull-left" - .prepend-left-20 - = f.label :issues_events, class: "label-light append-bottom-0" do - Issues events - %p.light - This url will be triggered when an issue is created/updated/merged - %div - = f.check_box :merge_requests_events, class: "pull-left" - .prepend-left-20 - = f.label :merge_requests_events, class: "label-light append-bottom-0" do - Merge Request events - %p.light - This url will be triggered when a merge request is created/updated/merged - %div - = f.check_box :build_events, class: "pull-left" - .prepend-left-20 - = f.label :build_events, class: "label-light append-bottom-0" do - Build events - %p.light - This url will be triggered when the build status changes - .form-group - = f.label :enable_ssl_verification, "SSL verification", class: "label-light" - %div - = f.check_box :enable_ssl_verification, class: "pull-left" - .prepend-left-20 - = f.label :enable_ssl_verification, class: "label-light append-bottom-0" do - Enable SSL verification - = f.submit "Add Webhook", class: "btn btn-create" - %hr - %h5.prepend-top-default - Webhooks (#{@hooks.count}) - - if @hooks.any? - %ul.well-list - - @hooks.each do |hook| - = render "project_hook", hook: hook - - else - %p.profile-settings-message.text-center.append-bottom-0 - No webhooks found, add one in the form above. += render 'shared/web_hooks/form', hook: @hook, hooks: @hooks, url_components: [@project.namespace.becomes(Namespace), @project] diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml new file mode 100644 index 00000000000..166dae248b6 --- /dev/null +++ b/app/views/projects/issues/_head.html.haml @@ -0,0 +1,25 @@ +%ul.nav-links.sub-nav + %div{ class: (container_class) } + - if project_nav_tab?(:issues) && !current_controller?(:merge_requests) + = nav_link(controller: :issues) do + = link_to url_for_project_issues(@project, only_path: true), title: 'Issues' do + %span + Issues + + - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests) + = nav_link(controller: :merge_requests) do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do + %span + Merge Requests + + - if project_nav_tab? :labels + = nav_link(controller: :labels) do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do + %span + Labels + + - if project_nav_tab? :milestones + = nav_link(controller: :milestones) do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do + %span + Milestones diff --git a/app/views/projects/issues/_header_title.html.haml b/app/views/projects/issues/_header_title.html.haml deleted file mode 100644 index 99f03549c44..00000000000 --- a/app/views/projects/issues/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Issues", namespace_project_issues_path(@project.namespace, @project)) diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 9ad86ed71c9..79b14819865 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -1,4 +1,4 @@ -%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue) } +%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } } - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project) .issue-check = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" @@ -6,7 +6,7 @@ .issue-title.title %span.issue-title-text = confidential_icon(issue) - = link_to_gfm issue.title, issue_path(issue) + = link_to issue.title, issue_path(issue) %ul.controls - if issue.closed? %li @@ -27,17 +27,11 @@ = icon('thumbs-down') = downvotes - - note_count = issue.notes.user.nonawards.count - - if note_count > 0 - %li - = link_to issue_path(issue) + "#notes" do - = icon('comments') - = note_count - - else - %li - = link_to issue_path(issue) + "#notes", class: "issue-no-comments" do - = icon('comments') - = note_count + - note_count = issue.notes.user.count + %li + = link_to issue_path(issue, anchor: 'notes'), class: ('issue-no-comments' if note_count.zero?) do + = icon('comments') + = note_count .issue-info #{issue.to_reference} · diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index d6b38b327ff..75f36579b11 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -2,12 +2,12 @@ %h2.merge-requests-title = pluralize(@merge_requests.count, 'Related Merge Request') %ul.unstyled-list - - has_any_ci = @merge_requests.any?(&:ci_commit) + - has_any_ci = @merge_requests.any?(&:pipeline) - @merge_requests.each do |merge_request| %li %span.merge-request-ci-status - - if merge_request.ci_commit - = render_ci_status(merge_request.ci_commit) + - if merge_request.pipeline + = render_pipeline_status(merge_request.pipeline) - elsif has_any_ci = icon('blank fw') %span.merge-request-id @@ -24,5 +24,8 @@ MERGED - elsif merge_request.closed? CLOSED - - if @closed_by_merge_requests.present? + %li = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count} + - if @closed_by_merge_requests.present? + %li + = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count} diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index 469429ccf3c..e93b7e0d66d 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -1,13 +1,13 @@ - if can?(current_user, :push_code, @project) .pull-right #new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)} - = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do + = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), + method: :post, class: 'btn has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do .checking - %i.fa.fa-spinner.fa-spin + = icon('spinner spin') Checking branches - .available(style="display: none") - %i.fa.fa-code-fork + .available.hide New branch - .unavailable(style="display: none") - %i.fa.fa-exclamation-triangle + .unavailable.hide + = icon('exclamation-triangle') New branch unavailable diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index bdfa0c7009e..b9bb6fe559d 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -5,10 +5,10 @@ - @related_branches.each do |branch| %li - sha = @project.repository.find_branch(branch).target - - ci_commit = @project.ci_commit(sha, branch) if sha - - if ci_commit + - pipeline = @project.pipeline(sha, branch) if sha + - if ci_copipelinemmit %span.related-branch-ci-status - = render_ci_status(ci_commit) + = render_pipeline_status(pipeline) %span.related-branch-info %strong = link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml index 20216297d25..7cf1923456e 100644 --- a/app/views/projects/issues/edit.html.haml +++ b/app/views/projects/issues/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Edit", "#{@issue.title} (##{@issue.iid})", "Issues" -= render "header_title" %h3.page-title Edit Issue ##{@issue.iid} diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder index ee8a9414657..7ad7c9c87e8 100644 --- a/app/views/projects/issues/index.atom.builder +++ b/app/views/projects/issues/index.atom.builder @@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id namespace_project_issues_url(@project.namespace, @project) xml.updated @issues.first.created_at.xmlschema if @issues.any? - @issues.each do |issue| - issue_to_atom(xml, issue) - end + xml << render(partial: 'issues/issue', collection: @issues) if @issues.any? end diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index efa7642b2dc..cd876b5ea62 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -1,25 +1,26 @@ +- @no_container = true - page_title "Issues" -= render "header_title" += render "projects/issues/head" = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues") -.top-area - = render 'shared/issuable/nav', type: :issues - .nav-controls - - if current_user - = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do - = icon('rss') - %span.icon-label - Subscribe +%div{ class: (container_class) } + .top-area + = render 'shared/issuable/nav', type: :issues + .nav-controls + - if current_user + = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do + = icon('rss') + %span.icon-label + Subscribe = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) - - if can? current_user, :create_issue, @project - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do - = icon('plus') - New Issue + - if can? current_user, :create_issue, @project + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do + New Issue -= render 'shared/issuable/filter', type: :issues + = render 'shared/issuable/filter', type: :issues -.issues-holder - = render "issues" + .issues-holder + = render "issues" diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml index b317a0c1cf4..e8aae0f47e2 100644 --- a/app/views/projects/issues/new.html.haml +++ b/app/views/projects/issues/new.html.haml @@ -1,5 +1,4 @@ - page_title "New Issue" -= render "header_title" %h3.page-title New Issue diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index bde80bbb54b..9b6a97c0959 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,7 +1,6 @@ - page_title "#{@issue.title} (##{@issue.iid})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes -- header_title project_title(@project, "Issues", namespace_project_issues_path(@project.namespace, @project)) .clearfix.detail-page-header .issuable-header @@ -39,26 +38,24 @@ %li = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) - if can?(current_user, :create_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do - = icon('plus') + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do New issue - if can?(current_user, :update_issue, @issue) - = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' - = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' - = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit' do - = icon('pencil-square-o') + = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' + = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' do Edit .issue-details.issuable-details .detail-page-description.content-block %h2.title - = markdown escape_once(@issue.title), pipeline: :single_line + = markdown escape_once(@issue.title), pipeline: :single_line, author: @issue.author - if @issue.description.present? .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } .wiki = preserve do - = markdown(@issue.description, cache_key: [@issue, "description"]) + = markdown(@issue.description, cache_key: [@issue, "description"], author: @issue.author) %textarea.hidden.js-task-list-field = @issue.description = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') @@ -71,7 +68,7 @@ .content-block.content-block-small = render 'new_branch' - = render 'votes/votes_block', votable: @issue + = render 'award_emoji/awards_block', awardable: @issue, inline: true %section.issuable-discussion = render 'projects/issues/discussion' diff --git a/app/views/projects/labels/_header_title.html.haml b/app/views/projects/labels/_header_title.html.haml deleted file mode 100644 index abe28da483b..00000000000 --- a/app/views/projects/labels/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Labels", namespace_project_labels_path(@project.namespace, @project)) diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 8bf544b8371..73c6f2a046c 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -1,28 +1,50 @@ -%li{id: dom_id(label)} +- label_css_id = dom_id(label) +%li{id: label_css_id, data: { id: label.id } } = render "shared/label_row", label: label - .pull-info-right - %span.append-right-20 - = link_to_label(label, type: :merge_request) do - = pluralize label.open_merge_requests_count, 'merge request' + .visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown + %button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } } + Options + %span.caret + .dropdown-menu.dropdown-menu-align-right + %ul + %li + = link_to_label(label, type: :merge_request) do + = pluralize label.open_merge_requests_count, 'merge request' + %li + = link_to_label(label) do + = pluralize label.open_issues_count(current_user), 'open issue' + - if current_user + %li.label-subscription{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } + %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } } + %span= label_subscription_toggle_button_text(label) + - if can? current_user, :admin_label, @project + %li + = link_to "Edit", edit_namespace_project_label_path(@project.namespace, @project, label) + %li + = link_to "Delete", namespace_project_label_path(@project.namespace, @project, label), title: "Delete", method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} - %span.append-right-20 - = link_to_label(label) do - = pluralize label.open_issues_count(current_user), 'open issue' + .pull-right.hidden-xs.hidden-sm.hidden-md + = link_to_label(label, type: :merge_request, css_class: 'btn btn-transparent btn-action') do + = pluralize label.open_merge_requests_count, 'merge request' + = link_to_label(label, css_class: 'btn btn-transparent btn-action') do + = pluralize label.open_issues_count(current_user), 'open issue' - if current_user - .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}} - .subscription-status{data: {status: label_subscription_status(label)}} - - %button.js-subscribe-button.label-subscribe-button.btn.action-buttons{ type: "button", data: { toggle: "tooltip" } } - %span= label_subscription_toggle_button_text(label) + .label-subscription.inline{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } } + %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } } + %span.sr-only= label_subscription_toggle_button_text(label) + = icon('eye', class: 'label-subscribe-button-icon') + = icon('spinner spin', class: 'label-subscribe-button-loading') - if can? current_user, :admin_label, @project - = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn action-buttons', data: {toggle: "tooltip"} do - %i.fa.fa-pencil-square-o - = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn action-buttons remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do - %i.fa.fa-trash-o + = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do + %span.sr-only Edit + = icon('pencil-square-o') + = link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do + %span.sr-only Delete + = icon('trash-o') -- if current_user - :javascript - new Subscription('##{dom_id(label)} .label-subscription'); + - if current_user + :javascript + new Subscription('##{dom_id(label)} .label-subscription'); diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml index 675a805e12f..6901ba13ab7 100644 --- a/app/views/projects/labels/edit.html.haml +++ b/app/views/projects/labels/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Edit", @label.name, "Labels" -= render "header_title" %h3.page-title Edit Label diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index cc41130a9dc..6e1baa46b05 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,23 +1,38 @@ +- @no_container = true - page_title "Labels" -= render "header_title" +- hide_class = '' += render "projects/issues/head" -.top-area - .nav-text - Labels can be applied to issues and merge requests. - .nav-controls - - if can? current_user, :admin_label, @project - = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do - = icon('plus') - New label +%div{ class: (container_class) } + .top-area + .nav-text + Labels can be applied to issues and merge requests. + .nav-controls + - if can?(current_user, :admin_label, @project) + = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do + New label -.labels - - if @labels.present? - %ul.content-list.manage-labels-list - = render @labels - = paginate @labels, theme: 'gitlab' - - else - .nothing-here-block - - if can? current_user, :admin_label, @project - Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels + .labels + - if can?(current_user, :admin_label, @project) + -# Only show it in the first page + - hide = @project.labels.empty? || (params[:page].present? && params[:page] != '1') + .prioritized-labels{ class: ('hide' if hide) } + %h5 Prioritized Labels + %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) } + - if @prioritized_labels.present? + = render @prioritized_labels + - else + %p.empty-message No prioritized labels yet + .other-labels + - if can?(current_user, :admin_label, @project) + %h5{ class: ('hide' if hide) } Other Labels + - if @labels.present? + %ul.content-list.manage-labels-list.js-other-labels + = render @labels + = paginate @labels, theme: 'gitlab' - else - No labels created + .nothing-here-block + - if can?(current_user, :admin_label, @project) + Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}. + - else + No labels created diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml index e20fd7d6891..49ddf901619 100644 --- a/app/views/projects/labels/new.html.haml +++ b/app/views/projects/labels/new.html.haml @@ -1,5 +1,4 @@ - page_title "New Label" -= render "header_title" %h3.page-title New Label diff --git a/app/views/projects/merge_requests/_head.html.haml b/app/views/projects/merge_requests/_head.html.haml deleted file mode 100644 index 19e4dab874b..00000000000 --- a/app/views/projects/merge_requests/_head.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -.top-tabs - = link_to namespace_project_merge_requests_path(@project.namespace, @project), class: "tab #{'active' if current_page?(namespace_project_merge_requests_path(@project.namespace, @project)) }" do - %span - Merge Requests - diff --git a/app/views/projects/merge_requests/_header_title.html.haml b/app/views/projects/merge_requests/_header_title.html.haml deleted file mode 100644 index 669a9b06bdf..00000000000 --- a/app/views/projects/merge_requests/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Merge Requests", namespace_project_merge_requests_path(@project.namespace, @project)) diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index e740fe8c84d..5029b365f93 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,7 +1,7 @@ %li{ class: mr_css_classes(merge_request) } .merge-request-title.title %span.merge-request-title-text - = link_to_gfm merge_request.title, merge_request_path(merge_request) + = link_to merge_request.title, merge_request_path(merge_request) %ul.controls - if merge_request.merged? %li @@ -11,9 +11,9 @@ = icon('ban') CLOSED - - if merge_request.ci_commit + - if merge_request.pipeline %li - = render_ci_status(merge_request.ci_commit) + = render_pipeline_status(merge_request.pipeline) - if merge_request.open? && merge_request.broken? %li @@ -35,17 +35,11 @@ = icon('thumbs-down') = downvotes - - note_count = merge_request.mr_and_commit_notes.user.nonawards.count - - if note_count > 0 - %li - = link_to merge_request_path(merge_request) + "#notes" do - = icon('comments') - = note_count - - else - %li - = link_to merge_request_path(merge_request) + "#notes", class: "merge-request-no-comments" do - = icon('comments') - = note_count + - note_count = merge_request.mr_and_commit_notes.user.count + %li + = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('merge-request-no-comments' if note_count.zero?) do + = icon('comments') + = note_count .merge-request-info #{merge_request.to_reference} · diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml index 5473fa19166..446887774a4 100644 --- a/app/views/projects/merge_requests/_merge_requests.html.haml +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -6,4 +6,3 @@ - if @merge_requests.present? = paginate @merge_requests, theme: "gitlab" - diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 18b3f9e1549..a5e67b95727 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -23,7 +23,7 @@ = link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits %span.badge= @commits.size - - if @ci_commit + - if @pipeline %li.builds-tab.active = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do Builds @@ -43,7 +43,7 @@ %p To preserve performance the line changes are not shown. - else = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @merge_request.diff_refs, show_whitespace_toggle: false - - if @ci_commit + - if @pipeline #builds.builds.tab-pane = render "projects/merge_requests/show/builds" diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 290753d57c6..c4df8bd504f 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,7 +1,6 @@ - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes -- header_title project_title(@project, "Merge Requests", namespace_project_merge_requests_path(@project.namespace, @project)) - if diff_view == 'parallel' - fluid_layout true @@ -15,13 +14,11 @@ - if @merge_request.open? .pull-right - if @merge_request.source_branch_exists? - = link_to "#modal_merge_info", class: "btn btn-sm", "data-toggle" => "modal" do - = icon('cloud-download fw') + = link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do Check out branch %span.dropdown %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } - = icon('download') Download as %span.caret %ul.dropdown-menu @@ -50,12 +47,12 @@ %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.nonawards.count + %span.badge= @merge_request.mr_and_commit_notes.user.count %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.size - - if @ci_commit + - if @pipeline %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 @@ -68,7 +65,7 @@ .tab-content #notes.notes.tab-pane.voting_notes .content-block.content-block-small.oneline-block - = render 'votes/votes_block', votable: @merge_request + = render 'award_emoji/awards_block', awardable: @merge_request, inline: true .row %section.col-md-12 diff --git a/app/views/projects/merge_requests/dropdowns/_branch.html.haml b/app/views/projects/merge_requests/dropdowns/_branch.html.haml index ba8d9a5835c..a60c445aa51 100644 --- a/app/views/projects/merge_requests/dropdowns/_branch.html.haml +++ b/app/views/projects/merge_requests/dropdowns/_branch.html.haml @@ -1,5 +1,5 @@ %ul - branches.each do |branch| %li - %a{ href: '#', class: "#{('is-active' if selected == branch)}", data: { id: branch } } + %a{ href: '#', class: "#{('is-active' if selected == branch)}", title: branch, data: { id: branch } } = branch diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml index b31ea5e5321..03159f123f3 100644 --- a/app/views/projects/merge_requests/edit.html.haml +++ b/app/views/projects/merge_requests/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Edit", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" -= render "header_title" %h3.page-title Edit Merge Request #{@merge_request.to_reference} diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index e56a44e0a79..9f948d41dda 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,20 +1,20 @@ +- @no_container = true - page_title "Merge Requests" -= render "header_title" - += render "projects/issues/head" = render 'projects/last_push' -.top-area - = render 'shared/issuable/nav', type: :merge_requests - .nav-controls - = render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project) +%div{ class: (container_class) } + .top-area + = render 'shared/issuable/nav', type: :merge_requests + .nav-controls + = render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project) - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - - if merge_project - = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do - = icon('plus') - New Merge Request + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - if merge_project + = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do + New Merge Request -= render 'shared/issuable/filter', type: :merge_requests + = render 'shared/issuable/filter', type: :merge_requests -.merge-requests-holder - = render 'merge_requests' + .merge-requests-holder + = render 'merge_requests' diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml index f5bf16ef3ad..a00d3128ffe 100644 --- a/app/views/projects/merge_requests/invalid.html.haml +++ b/app/views/projects/merge_requests/invalid.html.haml @@ -1,5 +1,4 @@ - page_title "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" -= render "header_title" .merge-request = render "projects/merge_requests/show/mr_title" diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml index 92ce479d463..84b6c9ebc5c 100644 --- a/app/views/projects/merge_requests/merge.js.haml +++ b/app/views/projects/merge_requests/merge.js.haml @@ -5,6 +5,9 @@ - when :merge_when_build_succeeds :plain $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_build_succeeds'))}"); +- when :sha_mismatch + :plain + $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/sha_mismatch'))}"); - else :plain $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}"); diff --git a/app/views/projects/merge_requests/new.html.haml b/app/views/projects/merge_requests/new.html.haml index d259968030e..2e798ce780a 100644 --- a/app/views/projects/merge_requests/new.html.haml +++ b/app/views/projects/merge_requests/new.html.haml @@ -1,5 +1,4 @@ - page_title "New Merge Request" -= render "header_title" - if @merge_request.can_be_created && !params[:change_branches] = render 'new_submit' diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml index a116ffe2e15..81de60f116c 100644 --- a/app/views/projects/merge_requests/show/_builds.html.haml +++ b/app/views/projects/merge_requests/show/_builds.html.haml @@ -1,2 +1,2 @@ -= render "projects/commit/ci_commit", ci_commit: @ci_commit, link_to_commit: true += render "projects/commit/pipeline", pipeline: @pipeline, link_to_commit: true diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index a23bd8d18d0..ebf18f6ac85 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -1,13 +1,13 @@ .detail-page-description.content-block %h2.title - = markdown escape_once(@merge_request.title), pipeline: :single_line + = markdown escape_once(@merge_request.title), pipeline: :single_line, author: @merge_request.author %div - if @merge_request.description.present? .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''} .wiki = preserve do - = markdown(@merge_request.description, cache_key: [@merge_request, "description"]) + = markdown(@merge_request.description, cache_key: [@merge_request, "description"], author: @merge_request.author) %textarea.hidden.js-task-list-field = @merge_request.description diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 36c275e8be1..5bf5210aeab 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -25,8 +25,7 @@ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' %li = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit' - = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request' - = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request' - = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit" do - = icon('pencil-square-o') + = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request' + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request' + = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit" do Edit diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 4d381754610..08a38d283d2 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,7 +1,7 @@ -- if @ci_commit +- if @pipeline .mr-widget-heading - %w[success skipped canceled failed running pending].each do |status| - .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @ci_commit.status == status) } + .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) } = ci_icon_for_status(status) %span CI build @@ -9,7 +9,7 @@ for - commit = @merge_request.last_commit = succeed "." do - = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace" + = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" %span.ci-coverage = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 55dbae598d3..0e0af57d76e 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -17,6 +17,8 @@ = render 'projects/merge_requests/widget/open/merge_when_build_succeeds' - elsif !@merge_request.can_be_merged_by?(current_user) = render 'projects/merge_requests/widget/open/not_allowed' + - elsif !@merge_request.mergeable_ci_state? && @pipeline && @pipeline.failed? + = render 'projects/merge_requests/widget/open/build_failed' - elsif @merge_request.can_be_merged? = render 'projects/merge_requests/widget/open/accept' @@ -26,4 +28,4 @@ %i.fa.fa-check Accepting this merge request will close #{"issue".pluralize(@closes_issues.size)} = succeed '.' do - != markdown issues_sentence(@closes_issues), pipeline: :gfm + != markdown issues_sentence(@closes_issues), pipeline: :gfm, author: @merge_request.author diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 3c68d61c4b5..d9efe81701f 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -13,7 +13,7 @@ check_enable: #{@merge_request.unchecked? ? "true" : "false"}, ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}", - ci_status: "", + ci_status: "#{@merge_request.pipeline ? @merge_request.pipeline.status : ''}", ci_message: { normal: "Build {{status}} for \"{{title}}\"", preparing: "{{status}} build for \"{{title}}\"" @@ -26,4 +26,10 @@ builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" }; + if (typeof merge_request_widget !== 'undefined') { + clearInterval(merge_request_widget.fetchBuildStatusInterval); + merge_request_widget.cancelPolling(); + merge_request_widget.clearEventListeners(); + } + merge_request_widget = new MergeRequestWidget(opts); diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 807833741af..941513febbd 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -1,31 +1,36 @@ -- status_class = @ci_commit ? " ci-#{@ci_commit.status}" : nil +- status_class = @pipeline ? " ci-#{@pipeline.status}" : nil = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f| = hidden_field_tag :authenticity_token, form_authenticity_token + = hidden_field_tag :sha, @merge_request.source_sha .accept-merge-holder.clearfix.js-toggle-container .clearfix .accept-action - - if @ci_commit && @ci_commit.active? + - if @pipeline && @pipeline.active? %span.btn-group = button_tag class: "btn btn-create js-merge-button merge_when_build_succeeds" do Merge When Build Succeeds - = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do - %span.caret - %span.sr-only - Select Merge Moment - %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' } - %li - = link_to "#", class: "merge_when_build_succeeds" do - = icon('check fw') - Merge When Build Succeeds - %li - = link_to "#", class: "accept_merge_request" do - = icon('warning fw') - Merge Immediately + - unless @project.only_allow_merge_if_build_succeeds? + = button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do + %span.caret + %span.sr-only + Select Merge Moment + %ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' } + %li + = link_to "#", class: "merge_when_build_succeeds" do + = icon('check fw') + Merge When Build Succeeds + %li + = link_to "#", class: "accept_merge_request" do + = icon('warning fw') + Merge Immediately - else = f.button class: "btn btn-create btn-grouped js-merge-button accept_merge_request #{status_class}" do Accept Merge Request - - if @merge_request.can_remove_source_branch?(current_user) + - if @merge_request.force_remove_source_branch? + .accept-control + The source branch will be removed. + - elsif @merge_request.can_remove_source_branch?(current_user) .accept-control.checkbox = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do = check_box_tag :should_remove_source_branch diff --git a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml new file mode 100644 index 00000000000..14f51af5360 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml @@ -0,0 +1,6 @@ +%h4 + = icon('exclamation-triangle') + The build for this merge request failed + +%p + Please retry the build or push a new commit to fix the failure. diff --git a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml index e6c089fefb2..06ab0a3fa00 100644 --- a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml +++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml @@ -1,9 +1,9 @@ -%h4 +%h4.has-conflicts = icon("exclamation-triangle") This merge request contains merge conflicts %p - Please resolve these conflicts or + Please resolve these conflicts or - if @merge_request.can_be_merged_by?(current_user) #{link_to "merge this request manually", "#modal_merge_info", class: "how_to_merge_link vlink", "data-toggle" => "modal"}. - else diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index 2168294c683..ad898ff153b 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -2,22 +2,21 @@ Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} to be merged automatically when the build succeeds. %div - - should_remove_source_branch = @merge_request.merge_params["should_remove_source_branch"].present? %p = succeed '.' do The changes will be merged into %span.label-branch= @merge_request.target_branch - - if should_remove_source_branch + - if @merge_request.remove_source_branch? The source branch will be removed. - else The source branch will not be removed. - - remove_source_branch_button = @merge_request.can_remove_source_branch?(current_user) && !should_remove_source_branch && @merge_request.merge_user == current_user + - remove_source_branch_button = !@merge_request.remove_source_branch? && @merge_request.can_remove_source_branch?(current_user) && @merge_request.merge_user == current_user - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_build_succeeds?(current_user) - if remove_source_branch_button || user_can_cancel_automatic_merge .clearfix.prepend-top-10 - if remove_source_branch_button - = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do + = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true, sha: @merge_request.source_sha), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do = icon('times') Remove Source Branch When Merged diff --git a/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml b/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml index a8145558ca8..57ce1959021 100644 --- a/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml +++ b/app/views/projects/merge_requests/widget/open/_not_allowed.html.haml @@ -1,4 +1,6 @@ -%h4 +%h4 Ready to be merged automatically %p Ask someone with write access to this repository to merge this request. + - if @merge_request.force_remove_source_branch? + The source branch will be removed. diff --git a/app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml b/app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml new file mode 100644 index 00000000000..499624f8dd8 --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_sha_mismatch.html.haml @@ -0,0 +1,6 @@ +%h4 + = icon("exclamation-triangle") + This merge request has received new commits since the page was loaded. + +%p + Please reload the page to review the new commits before merging. diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 687222fa92f..f5e2b927da8 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -17,9 +17,8 @@ .col-md-6 .form-group = f.label :due_date, "Due Date", class: "control-label" - .col-sm-10= f.hidden_field :due_date .col-sm-10 - .datepicker + = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date" .form-actions - if @milestone.new_record? diff --git a/app/views/projects/milestones/_header_title.html.haml b/app/views/projects/milestones/_header_title.html.haml deleted file mode 100644 index 5f4b6982a6d..00000000000 --- a/app/views/projects/milestones/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Milestones", namespace_project_milestones_path(@project.namespace, @project)) diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml index 43f8863163d..be682226ab6 100644 --- a/app/views/projects/milestones/edit.html.haml +++ b/app/views/projects/milestones/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Edit", @milestone.title, "Milestones" -= render "header_title" %h3.page-title Edit Milestone ##{@milestone.iid} diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index abe567af1dd..b0e0bdfff5a 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,22 +1,22 @@ +- @no_container = true - page_title "Milestones" -= render "header_title" += render "projects/issues/head" +%div{ class: (container_class) } + .top-area + = render 'shared/milestones_filter' -.top-area - = render 'shared/milestones_filter' + .nav-controls + - if can?(current_user, :admin_milestone, @project) + = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do + New Milestone - .nav-controls - - if can?(current_user, :admin_milestone, @project) - = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do - = icon('plus') - New Milestone + .milestones + %ul.content-list + = render @milestones -.milestones - %ul.content-list - = render @milestones + - if @milestones.blank? + %li + .nothing-here-block No milestones to show - - if @milestones.blank? - %li - .nothing-here-block No milestones to show - - = paginate @milestones, theme: "gitlab" + = paginate @milestones, theme: "gitlab" diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml index 0d016f78313..7f372b41698 100644 --- a/app/views/projects/milestones/new.html.haml +++ b/app/views/projects/milestones/new.html.haml @@ -1,5 +1,4 @@ - page_title "New Milestone" -= render "header_title" %h3.page-title New Milestone diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 6ec84660157..73772cc0e32 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -1,14 +1,12 @@ - page_title @milestone.title, "Milestones" - page_description @milestone.description -= render "header_title" - .detail-page-header .status-box{ class: status_box_class(@milestone) } - if @milestone.closed? Closed - elsif @milestone.expired? - Expired + Past due - else Open %span.identifier @@ -25,11 +23,9 @@ = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do - = icon('pencil-square-o') Edit = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do - = icon('trash-o') Delete .detail-page-description.milestone-detail diff --git a/app/views/projects/network/_head.html.haml b/app/views/projects/network/_head.html.haml index c609c505def..86295a3d011 100644 --- a/app/views/projects/network/_head.html.haml +++ b/app/views/projects/network/_head.html.haml @@ -1,6 +1,9 @@ -.row-content-block.append-bottom-default - .tree-ref-holder - = render partial: 'shared/ref_switcher', locals: {destination: 'graph'} +- @no_container = true - .oneline - You can move around the graph by using the arrow keys. +%div{ class: (container_class) } + .row-content-block.second-block.content-component-block + .tree-ref-holder + = render partial: 'shared/ref_switcher', locals: {destination: 'graph'} + + .oneline + You can move around the graph by using the arrow keys. diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 8065663ca2a..bf9baaea889 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,21 +1,21 @@ - page_title "Network", @ref -= render "projects/commits/header_title" = render "projects/commits/head" = render "head" -.project-network - .controls - = form_tag namespace_project_network_path(@project.namespace, @project, @id), method: :get, class: 'form-inline network-form' do |f| - = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Input an extended SHA1 syntax", class: 'search-input form-control input-mx-250 search-sha' - = button_tag class: 'btn btn-success' do - = icon('search') - .inline.prepend-left-20 - .checkbox.light - = label_tag :filter_ref do - = check_box_tag :filter_ref, 1, @options[:filter_ref] - %span Begin with the selected commit +%div{ class: (container_class) } + .project-network + .controls + = form_tag namespace_project_network_path(@project.namespace, @project, @id), method: :get, class: 'form-inline network-form' do |f| + = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Input an extended SHA1 syntax", class: 'search-input form-control input-mx-250 search-sha' + = button_tag class: 'btn btn-success' do + = icon('search') + .inline.prepend-left-20 + .checkbox.light + = label_tag :filter_ref do + = check_box_tag :filter_ref, 1, @options[:filter_ref] + %span Begin with the selected commit - .network-graph - = spinner nil, true + .network-graph + = spinner nil, true :javascript network_graph = new Network({ diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index a4c6094c69a..f9ac16b32f3 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -1,5 +1,5 @@ - page_title 'New Project' -- header_title "Projects", root_path +- header_title "Projects", dashboard_projects_path %h3.page-title New Project diff --git a/app/views/projects/notes/_diff_notes_with_reply.html.haml b/app/views/projects/notes/_diff_notes_with_reply.html.haml index 39be072855a..8144c1ba49e 100644 --- a/app/views/projects/notes/_diff_notes_with_reply.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply.html.haml @@ -1,10 +1,8 @@ -- note = notes.first # example note --# Check if line want not changed since comment was left -- if !defined?(line) || line == note.diff_line - %tr.notes_holder - %td.notes_line{ colspan: 2 } - %td.notes_content - %ul.notes{ data: { discussion_id: note.discussion_id } } - = render notes - .discussion-reply-holder - = link_to_reply_diff(note) +- note = notes.first +%tr.notes_holder + %td.notes_line{ colspan: 2 } + %td.notes_content + %ul.notes{ data: { discussion_id: note.discussion_id } } + = render partial: "projects/notes/note", collection: notes, as: :note + .discussion-reply-holder + = link_to_reply_discussion(note) diff --git a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml index f8aa5e2fa7d..45986b0d1e8 100644 --- a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml @@ -1,27 +1,27 @@ -- note1 = notes_left.present? ? notes_left.first : nil -- note2 = notes_right.present? ? notes_right.first : nil +- note_left = notes_left.present? ? notes_left.first : nil +- note_right = notes_right.present? ? notes_right.first : nil %tr.notes_holder - - if note1 + - if note_left %td.notes_line.old %td.notes_content.parallel.old - %ul.notes{ data: { discussion_id: note1.discussion_id } } - = render notes_left + %ul.notes{ data: { discussion_id: note_left.discussion_id } } + = render partial: "projects/notes/note", collection: notes_left, as: :note .discussion-reply-holder - = link_to_reply_diff(note1, 'old') + = link_to_reply_discussion(note_left, 'old') - else %td.notes_line.old= "" %td.notes_content.parallel.old= "" - - if note2 + - if note_right %td.notes_line.new %td.notes_content.parallel.new - %ul.notes{ data: { discussion_id: note2.discussion_id } } - = render notes_right + %ul.notes{ data: { discussion_id: note_right.discussion_id } } + = render partial: "projects/notes/note", collection: notes_right, as: :note .discussion-reply-holder - = link_to_reply_diff(note2, 'new') + = link_to_reply_discussion(note_right, 'new') - else %td.notes_line.new= "" %td.notes_content.parallel.new= "" diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml index 572b00a38c7..7869d6413d8 100644 --- a/app/views/projects/notes/_discussion.html.haml +++ b/app/views/projects/notes/_discussion.html.haml @@ -1,13 +1,46 @@ - note = discussion_notes.first +- expanded = !note.diff_note? || note.active? %li.note.note-discussion.timeline-entry .timeline-entry-inner .timeline-icon = link_to user_path(note.author) do - = image_tag avatar_icon(note.author_email), class: "avatar s40" + = image_tag avatar_icon(note.author), class: "avatar s40" .timeline-content - - if note.for_merge_request? - - (active_notes, outdated_notes) = discussion_notes.partition(&:active?) - = render "projects/notes/discussions/active", discussion_notes: active_notes if active_notes.length > 0 - = render "projects/notes/discussions/outdated", discussion_notes: outdated_notes if outdated_notes.length > 0 - - else - = render "projects/notes/discussions/commit", discussion_notes: discussion_notes + .discussion.js-toggle-container{ class: note.discussion_id } + .discussion-header + = link_to_member(@project, note.author, avatar: false) + + .inline.discussion-headline-light + = note.author.to_reference + started a discussion on + + - if note.for_commit? + - commit = note.noteable + - if commit + commit + = link_to commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code), class: 'monospace' + - else + a deleted commit + - else + - if note.active? + = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do + the diff + - else + an outdated diff + + = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "note-created-ago") + + .discussion-actions + = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do + - if expanded + = icon("chevron-up") + - else + = icon("chevron-down") + + Toggle discussion + + .discussion-body.js-toggle-content{ class: ("hide" unless expanded) } + - if note.diff_note? + = render "projects/notes/discussions/diff_with_notes", discussion_notes: discussion_notes + - else + = render "projects/notes/discussions/notes", discussion_notes: discussion_notes diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index d0ac380f216..67ed38a7b22 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -6,6 +6,7 @@ = f.hidden_field :line_code = f.hidden_field :noteable_id = f.hidden_field :noteable_type + = f.hidden_field :type = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..." diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index aeb7c1d5ee4..bcdbff08011 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -1,5 +1,8 @@ +- return unless note.author +- return if note.cross_reference_not_visible_for?(current_user) + - note_editable = note_editable?(note) -%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} } +%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} } .timeline-entry-inner .timeline-icon %a{href: user_path(note.author)} @@ -8,28 +11,33 @@ .note-header = link_to_member(note.project, note.author, avatar: false) .inline.note-headline-light - = "#{note.author.to_reference}" - - if !note.system + = note.author.to_reference + - unless note.system commented %a{ href: "##{dom_id(note)}" } = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') .note-actions - access = note.project.team.human_max_access(note.author.id) - if access - %span.note-role - = access + %span.note-role.hidden-xs= access + - if current_user + = link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do + = icon('spinner spin') + = icon('smile-o') - if note_editable = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = icon('pencil') - = 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 js-note-delete danger' do + = 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') .note-body{class: note_editable ? 'js-task-list-container' : ''} .note-text = preserve do - = markdown(note.note, pipeline: :note, cache_key: [note, "note"]) + = markdown(note.note, pipeline: :note, cache_key: [note, "note"], author: note.author) + = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note_editable = render 'projects/notes/edit_form', note: note - = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) + .note-awards + = render 'award_emoji/awards_block', awardable: note, inline: false - if note.attachment.url .note-attachment diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/projects/notes/_notes.html.haml index 62db86fb181..ebf7e8a9cb3 100644 --- a/app/views/projects/notes/_notes.html.haml +++ b/app/views/projects/notes/_notes.html.haml @@ -2,14 +2,9 @@ - @discussions.each do |discussion_notes| - note = discussion_notes.first - if note_for_main_target?(note) - - next if note.cross_reference_not_visible_for?(current_user) - - = render discussion_notes + = render partial: "projects/notes/note", object: note, as: :note - else = render 'projects/notes/discussion', discussion_notes: discussion_notes - else - @notes.each do |note| - - next unless note.author - - next if note.cross_reference_not_visible_for?(current_user) - - = render note + = render partial: "projects/notes/note", object: note, as: :note diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml deleted file mode 100644 index 0ea8862a684..00000000000 --- a/app/views/projects/notes/discussions/_active.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -- note = discussion_notes.first -.discussion.js-toggle-container{ class: note.discussion_id } - .discussion-header - = link_to_member(@project, note.author, avatar: false) - .inline.discussion-headline-light - = "#{note.author.to_reference} started a discussion" - = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do - on the diff - = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") - .discussion-actions - = link_to "#", class: "discussion-action-button discussion-toggle-button js-toggle-button" do - %i.fa.fa-chevron-up - Show/hide discussion - - .discussion-body.js-toggle-content - = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml deleted file mode 100644 index 2a2ead58eeb..00000000000 --- a/app/views/projects/notes/discussions/_commit.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -- note = discussion_notes.first -- commit = note.noteable -- commit_description = commit ? 'commit' : 'a deleted commit' -.discussion.js-toggle-container{ class: note.discussion_id } - .discussion-header - = link_to_member(@project, note.author, avatar: false) - .inline.discussion-headline-light - = "#{note.author.to_reference} started a discussion on #{commit_description}" - - if commit - = link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace') - = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") - .discussion-actions - = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do - %i.fa.fa-chevron-up - Show/hide discussion - .discussion-body.js-toggle-content - - if note.for_diff_line? - = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note - - else - .panel.panel-default - .notes{ data: { discussion_id: discussion_notes.first.discussion_id } } - %ul.notes.timeline - = render discussion_notes - .discussion-reply-holder - = link_to_reply_diff(discussion_notes.first) diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml deleted file mode 100644 index d46aab000c3..00000000000 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -- diff = note.diff -- if diff - .diff-file - .diff-header - %span - - if diff.deleted_file - = diff.old_path - - else - = diff.new_path - - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode - %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" - .diff-content.code.js-syntax-highlight - %table - - note.truncated_diff_lines.each do |line| - - type = line.type - - line_code = generate_line_code(note.file_path, line) - %tr.line_holder{ id: line_code, class: "#{type}" } - - if type == "match" - %td.old_line.diff-line-num= "..." - %td.new_line.diff-line-num= "..." - %td.line_content.match= line.text - - else - %td.old_line.diff-line-num{ data: { linenumber: type == "new" ? " ".html_safe : line.old_pos } } - %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? " ".html_safe : line.new_pos } } - %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type) - - - if line_code == note.line_code - = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/app/views/projects/notes/discussions/_diff_with_notes.html.haml b/app/views/projects/notes/discussions/_diff_with_notes.html.haml new file mode 100644 index 00000000000..6401245bf73 --- /dev/null +++ b/app/views/projects/notes/discussions/_diff_with_notes.html.haml @@ -0,0 +1,30 @@ +- note = discussion_notes.first +- diff = note.diff +- return unless diff + +.diff-file + .diff-header + %span + - if diff.deleted_file + = diff.old_path + - else + = diff.new_path + - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode + %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" + .diff-content.code.js-syntax-highlight + %table + - note.truncated_diff_lines.each do |line| + - type = line.type + - line_code = generate_line_code(note.diff_file_path, line) + %tr.line_holder{ id: line_code, class: "#{type}" } + - if type == "match" + %td.old_line.diff-line-num= "..." + %td.new_line.diff-line-num= "..." + %td.line_content.match= line.text + - else + %td.old_line.diff-line-num{ data: { linenumber: type == "new" ? " ".html_safe : line.old_pos } } + %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? " ".html_safe : line.new_pos } } + %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type) + + - if line_code == note.line_code + = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/app/views/projects/notes/discussions/_notes.html.haml b/app/views/projects/notes/discussions/_notes.html.haml new file mode 100644 index 00000000000..e598e3c7c63 --- /dev/null +++ b/app/views/projects/notes/discussions/_notes.html.haml @@ -0,0 +1,7 @@ +- note = discussion_notes.first +.panel.panel-default + .notes{ data: { discussion_id: note.discussion_id } } + %ul.notes.timeline + = render partial: "projects/notes/note", collection: discussion_notes, as: :note + .discussion-reply-holder + = link_to_reply_discussion(note) diff --git a/app/views/projects/notes/discussions/_outdated.html.haml b/app/views/projects/notes/discussions/_outdated.html.haml deleted file mode 100644 index 45141bcd1df..00000000000 --- a/app/views/projects/notes/discussions/_outdated.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -- note = discussion_notes.first -.discussion.js-toggle-container{ class: note.discussion_id } - .discussion-header - = link_to_member(@project, note.author, avatar: false) - .inline.discussion-headline-light - = "#{note.author.to_reference} started a discussion" - on the outdated diff - = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "discussion_updated_ago") - .discussion-actions - = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do - %i.fa.fa-chevron-down - Show/hide discussion - .discussion-body.js-toggle-content.hide - = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml new file mode 100644 index 00000000000..f278d4e0538 --- /dev/null +++ b/app/views/projects/pipelines/_head.html.haml @@ -0,0 +1,15 @@ +%ul.nav-links.sub-nav + %div{ class: (container_class) } + - if project_nav_tab? :pipelines + = nav_link(controller: :pipelines) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + %span + Pipelines + %span.badge.count.ci_counter= number_with_delimiter(@project.pipelines.running_or_pending.count) + + - if project_nav_tab? :builds + = nav_link(controller: %w(builds)) do + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do + %span + Builds + %span.badge.count.builds_counter= number_with_delimiter(@project.running_or_pending_build_count) diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml new file mode 100644 index 00000000000..8289aefcde7 --- /dev/null +++ b/app/views/projects/pipelines/_info.html.haml @@ -0,0 +1,37 @@ +%p +.commit-info-row + Pipeline + = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @pipeline.id), class: "monospace" + with + = pluralize @pipeline.statuses.count(:id), "build" + - if @pipeline.ref + for + = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace" + - if @pipeline.duration + in + = time_interval_in_words @pipeline.duration + + .pull-right + = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do + = ci_icon_for_status(@pipeline.status) + = ci_label_for_status(@pipeline.status) + +- if @commit + .commit-info-row + %span.light Authored by + %strong + = commit_author_link(@commit, avatar: true, size: 24) + #{time_ago_with_tooltip(@commit.authored_date)} + +.commit-info-row + %span.light Commit + = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace" + = clipboard_button(clipboard_text: @pipeline.sha) + +- if @commit + .commit-box.content-block + %h3.commit-title + = markdown escape_once(@commit.title), pipeline: :single_line + - if @commit.description.present? + %pre.commit-description + = preserve(markdown(escape_once(@commit.description), pipeline: :single_line)) diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml new file mode 100644 index 00000000000..b70693eeb62 --- /dev/null +++ b/app/views/projects/pipelines/index.html.haml @@ -0,0 +1,58 @@ +- @no_container = true +- page_title "Pipelines" += render "projects/pipelines/head" + +%div{ class: (container_class) } + .top-area + %ul.nav-links + %li{class: ('active' if @scope.nil?)} + = link_to project_pipelines_path(@project) do + All + %span.badge.js-totalbuilds-count + = number_with_delimiter(@pipelines_count) + + %li{class: ('active' if @scope == 'running')} + = link_to project_pipelines_path(@project, scope: :running) do + Running + %span.badge.js-running-count + = number_with_delimiter(@running_or_pending_count) + + %li{class: ('active' if @scope == 'branches')} + = link_to project_pipelines_path(@project, scope: :branches) do + Branches + + %li{class: ('active' if @scope == 'tags')} + = link_to project_pipelines_path(@project, scope: :tags) do + Tags + + .nav-controls + - if can? current_user, :create_pipeline, @project + = link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do + New pipeline + + - unless @repository.gitlab_ci_yml + = link_to 'Get started with Pipelines', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info' + + = link_to ci_lint_path, class: 'btn btn-default' do + %span CI Lint + + %ul.content-list.pipelines + - stages = @pipelines.stages + - if @pipelines.blank? + %li + .nothing-here-block No pipelines to show + - else + .table-holder + %table.table.builds + %tbody + %th ID + %th Commit + - stages.each do |stage| + %th.stage + %span.has-tooltip{ title: "#{stage.titleize}" } + = stage.titleize.pluralize + %th Duration + %th + = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages + + = paginate @pipelines, theme: 'gitlab' diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml new file mode 100644 index 00000000000..5f4ec2e40c8 --- /dev/null +++ b/app/views/projects/pipelines/new.html.haml @@ -0,0 +1,21 @@ +- page_title "New Pipeline" + +%h3.page-title + New Pipeline +%hr + += form_for @pipeline, as: :pipeline, url: namespace_project_pipelines_path(@project.namespace, @project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f| + = form_errors(@pipeline) + .form-group + = f.label :ref, 'Create for', class: 'control-label' + .col-sm-10 + = f.text_field :ref, required: true, tabindex: 2, class: 'form-control' + .help-block Existing branch name, tag + .form-actions + = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3 + = link_to 'Cancel', namespace_project_pipelines_path(@project.namespace, @project), class: 'btn btn-cancel' + +:javascript + var availableRefs = #{@project.repository.ref_names.to_json}; + + new NewBranchForm($('.js-new-pipeline-form'), availableRefs) diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml new file mode 100644 index 00000000000..75943c64276 --- /dev/null +++ b/app/views/projects/pipelines/show.html.haml @@ -0,0 +1,8 @@ +- page_title "Pipeline" + +.prepend-top-default + - if @commit + = render "projects/pipelines/info" + %div.block-connector + += render "projects/commit/pipeline", pipeline: @pipeline diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml index c53033e367c..6671ee2c6d6 100644 --- a/app/views/projects/project_members/_group_members.html.haml +++ b/app/views/projects/project_members/_group_members.html.haml @@ -7,7 +7,6 @@ - if can?(current_user, :admin_group_member, @group) .controls = link_to group_group_members_path(@group), class: 'btn' do - = icon('pencil-square-o') Manage group members %ul.content-list - members.limit(20).each do |member| diff --git a/app/views/projects/project_members/_header_title.html.haml b/app/views/projects/project_members/_header_title.html.haml deleted file mode 100644 index a31f0a37fa2..00000000000 --- a/app/views/projects/project_members/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Members", namespace_project_project_members_path(@project.namespace, @project)) diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml index 05bf3a7ef6a..268f140d7db 100644 --- a/app/views/projects/project_members/_project_member.html.haml +++ b/app/views/projects/project_members/_project_member.html.haml @@ -32,9 +32,9 @@ .pull-right %strong= member.human_access - if can?(current_user, :update_project_member, member) - = button_tag class: "btn-xs btn js-toggle-button", + = button_tag class: "btn-xs btn-grouped inline btn js-toggle-button", title: 'Edit access level', type: 'button' do - %i.fa.fa-pencil-square-o + = icon('pencil') - if can?(current_user, :destroy_project_member, member) @@ -44,7 +44,7 @@ Leave - else = link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do - %i.fa.fa-minus.fa-inverse + = icon('trash') .edit-member.hide.js-toggle-content %br diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml index 189906498cb..eef97107d77 100644 --- a/app/views/projects/project_members/import.html.haml +++ b/app/views/projects/project_members/import.html.haml @@ -1,5 +1,4 @@ - page_title "Import members" -= render "header_title" %h3.page-title Import members from another project diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index ebcfc907ebb..15dc064e7ea 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,5 +1,4 @@ - page_title "Members" -= render "header_title" .project-members-page.prepend-top-default - if can?(current_user, :admin_project_member, @project) diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index b9e9dd8aaea..565905cbe7b 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -1,7 +1,7 @@ %h5.prepend-top-0 Already Protected (#{@branches.size}) - if @branches.empty? - %p.profile-settings-message.text-center + %p.settings-message.text-center No branches are protected, protect a branch with the form above. - else - can_admin_project = can?(current_user, :admin_project, @project) diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index 0d59cec322c..835398b6f98 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Edit", @tag.name, "Tags" -= render "projects/commits/header_title" = render "projects/commits/head" .row-content-block diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml index 6ca919f7f80..43a6fdfd103 100644 --- a/app/views/projects/repositories/_feed.html.haml +++ b/app/views/projects/repositories/_feed.html.haml @@ -12,7 +12,7 @@ = link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do %code= commit.short_id = image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: '' - = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line + = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line, author: commit.author %td %span.pull-right.cgray = time_ago_with_tooltip(commit.committed_date) diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml new file mode 100644 index 00000000000..d62f5c8f131 --- /dev/null +++ b/app/views/projects/runners/_form.html.haml @@ -0,0 +1,32 @@ += form_for runner, url: runner_form_url, html: { class: 'form-horizontal' } do |f| + = form_errors(runner) + .form-group + = label :active, "Active", class: 'control-label' + .col-sm-10 + .checkbox + = f.check_box :active + %span.light Paused runners don't accept new builds + .form-group + = label :run_untagged, 'Run untagged jobs', class: 'control-label' + .col-sm-10 + .checkbox + = f.check_box :run_untagged + %span.light Indicates whether this runner can pick jobs without tags + .form-group + = label_tag :token, class: 'control-label' do + Token + .col-sm-10 + = f.text_field :token, class: 'form-control', readonly: true + .form-group + = label_tag :description, class: 'control-label' do + Description + .col-sm-10 + = f.text_field :description, class: 'form-control' + .form-group + = label_tag :tag_list, class: 'control-label' do + Tags + .col-sm-10 + = f.text_field :tag_list, value: runner.tag_list.to_s, class: 'form-control' + .help-block You can setup jobs to only use runners with specific tags + .form-actions + = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 47ec420189d..96e2aac451f 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -5,7 +5,7 @@ - if @runners.include?(runner) = link_to runner.short_sha, runner_path(runner) %small - =link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do + = link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do %i.fa.fa-edit.btn - else = runner.short_sha diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index 30cd1263a12..8ae9f0d95f7 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -8,7 +8,7 @@ Install GitLab Runner software. Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it %li - Specify following URL during runner setup: + Specify the following URL during runner setup: %code #{ci_root_url(only_path: false)} %li Use the following registration token during setup: diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml index eba03028af8..95706888655 100644 --- a/app/views/projects/runners/edit.html.haml +++ b/app/views/projects/runners/edit.html.haml @@ -1,29 +1,6 @@ - page_title "Edit", "#{@runner.description} ##{@runner.id}", "Runners" %h4 Runner ##{@runner.id} + %hr -= form_for @runner, url: runner_path(@runner), html: { class: 'form-horizontal' } do |f| - .form-group - = label :active, "Active", class: 'control-label' - .col-sm-10 - .checkbox - = f.check_box :active - %span.light Paused runners don't accept new builds - .form-group - = label_tag :token, class: 'control-label' do - Token - .col-sm-10 - = f.text_field :token, class: 'form-control', readonly: true - .form-group - = label_tag :description, class: 'control-label' do - Description - .col-sm-10 - = f.text_field :description, class: 'form-control' - .form-group - = label_tag :tag_list, class: 'control-label' do - Tags - .col-sm-10 - = f.text_field :tag_list, value: @runner.tag_list.to_s, class: 'form-control' - .help-block You can setup jobs to only use runners with specific tags - .form-actions - = f.submit 'Save changes', class: 'btn btn-save' + = render 'form', runner: @runner, runner_form_url: runner_path(@runner) diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml index 5bf4c09ca25..f24e1b9144e 100644 --- a/app/views/projects/runners/show.html.haml +++ b/app/views/projects/runners/show.html.haml @@ -17,50 +17,39 @@ %th Property Name %th Value %tr - %td - Tags + %td Active + %td= @runner.active? ? 'Yes' : 'No' + %tr + %td Can run untagged jobs + %td= @runner.run_untagged? ? 'Yes' : 'No' + %tr + %td Tags %td - @runner.tag_list.each do |tag| %span.label.label-primary = tag %tr - %td - Name - %td - = @runner.name + %td Name + %td= @runner.name %tr - %td - Version - %td - = @runner.version + %td Version + %td= @runner.version %tr - %td - Revision - %td - = @runner.revision + %td Revision + %td= @runner.revision %tr - %td - Platform - %td - = @runner.platform + %td Platform + %td= @runner.platform %tr - %td - Architecture - %td - = @runner.architecture + %td Architecture + %td= @runner.architecture %tr - %td - Description - %td - = @runner.description + %td Description + %td= @runner.description %tr - %td - Last contact + %td Last contact %td - if @runner.contacted_at #{time_ago_in_words(@runner.contacted_at)} ago - else Never - - - diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 1b70880043a..1f13ea28b4e 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -1,18 +1,16 @@ -%h3.page-title - = @service.title - = boolean_to_icon @service.activated? +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + = @service.title + = boolean_to_icon @service.activated? -%p= @service.description - -%hr - -= form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form| - = render 'shared/service_settings', form: form - - .form-actions - = form.submit 'Save changes', class: 'btn btn-save' - - - if @service.valid? && @service.activated? - - disabled = @service.can_test? ? '':'disabled' - = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service.to_param), class: "btn #{disabled}" - = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel" + %p= @service.description + .col-lg-9 + = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form| + = render 'shared/service_settings', form: form + = form.submit 'Save changes', class: 'btn btn-save' + + - if @service.valid? && @service.activated? + - disabled = @service.can_test? ? '':'disabled' + = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service.to_param), class: "btn #{disabled}" + = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/index.html.haml index c1356f6db02..4a33a5bc6f6 100644 --- a/app/views/projects/services/index.html.haml +++ b/app/views/projects/services/index.html.haml @@ -1,24 +1,32 @@ - page_title "Services" -%h3.page-title Project services -%p.light Project services allow you to integrate GitLab with other applications -.table-holder - %table.table - %thead - %tr - %th - %th Service - %th Description - %th Last edit - - @services.sort_by(&:title).each do |service| - %tr - %td - = boolean_to_icon service.activated? - %td - = link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do - %strong= service.title - %td - = service.description - %td.light - = time_ago_in_words service.updated_at - ago +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + Project services + %p Project services allow you to integrate GitLab with other applications + .col-lg-9 + %table.table + %colgroup + %col + %col + %col.hidden-xs + %col{ width: "120" } + %thead + %tr + %th + %th Service + %th.hidden-xs Description + %th Last edit + - @services.sort_by(&:title).each do |service| + %tr + %td + = boolean_to_icon service.activated? + %td + = link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do + %strong= service.title + %td.hidden-xs + = service.description + %td.light + = time_ago_in_words service.updated_at + ago diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder index 9b3d3f069d9..11310d5e1e1 100644 --- a/app/views/projects/show.atom.builder +++ b/app/views/projects/show.atom.builder @@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id namespace_project_url(@project.namespace, @project) xml.updated @events[0].updated_at.xmlschema if @events[0] - @events.each do |event| - event_to_atom(xml, event) - end + xml << render(@events) if @events.any? end diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 74feb9e3282..4afa902b4eb 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -13,50 +13,50 @@ = render "home_panel" .project-stats.row-content-block.second-block - %ul.nav - %li - = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do - = pluralize(number_with_delimiter(@project.commit_count), 'commit') - %li - = link_to namespace_project_branches_path(@project.namespace, @project) do - = pluralize(number_with_delimiter(@repository.branch_names.count), 'branch') - %li - = link_to namespace_project_tags_path(@project.namespace, @project) do - = pluralize(number_with_delimiter(@repository.tag_names.count), 'tag') - - %li - = link_to project_files_path(@project) do - = repository_size - - - if default_project_view != 'readme' && @repository.readme + %div{ class: (container_class) } + %ul.nav %li - = link_to 'Readme', readme_path(@project) - - - if @repository.changelog + = link_to project_files_path(@project) do + Files (#{repository_size}) %li - = link_to 'Changelog', changelog_path(@project) - - - if @repository.license_blob + = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do + #{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)}) %li - = link_to license_short_name(@project), license_path(@project) - - - if @repository.contribution_guide + = link_to namespace_project_branches_path(@project.namespace, @project) do + #{'Branch'.pluralize(@repository.branch_names.count)} (#{number_with_delimiter(@repository.branch_names.count)}) %li - = link_to 'Contribution guide', contribution_guide_path(@project) + = link_to namespace_project_tags_path(@project.namespace, @project) do + #{'Tag'.pluralize(@repository.tag_names.count)} (#{number_with_delimiter(@repository.tag_names.count)}) + + - if default_project_view != 'readme' && @repository.readme + %li + = link_to 'Readme', readme_path(@project) + + - if @repository.changelog + %li + = link_to 'Changelog', changelog_path(@project) + + - if @repository.license_blob + %li + = link_to license_short_name(@project), license_path(@project) + + - if @repository.contribution_guide + %li + = link_to 'Contribution guide', contribution_guide_path(@project) - - if current_user && can_push_branch?(@project, @project.default_branch) - - unless @repository.changelog - %li.missing - = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do - Add Changelog - - unless @repository.license_blob - %li.missing - = link_to add_special_file_path(@project, file_name: 'LICENSE') do - Add License - - unless @repository.contribution_guide - %li.missing - = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do - Add Contribution guide + - if current_user && can_push_branch?(@project, @project.default_branch) + - unless @repository.changelog + %li.missing + = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do + Add Changelog + - unless @repository.license_blob + %li.missing + = link_to add_special_file_path(@project, file_name: 'LICENSE') do + Add License + - unless @repository.contribution_guide + %li.missing + = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do + Add Contribution guide - if @repository.commit .content-block.second-block.white diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 4a515469422..bf57beb9d07 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -1,11 +1,27 @@ -= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do - = icon('plus') - New Snippet -- if can?(current_user, :admin_project_snippet, @snippet) - = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' do - = icon('trash-o') - Delete -- if can?(current_user, :update_project_snippet, @snippet) - = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do - = icon('pencil-square-o') - Edit +.hidden-xs + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New Snippet" do + = icon('plus') + New Snippet + - if can?(current_user, :update_project_snippet, @snippet) + = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do + Edit + - if can?(current_user, :update_project_snippet, @snippet) + = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do + Delete +.visible-xs-block.dropdown + %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } + Options + %span.caret + .dropdown-menu.dropdown-menu-full-width + %ul + %li + = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New Snippet" do + New Snippet + - if can?(current_user, :update_project_snippet, @snippet) + %li + = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do + Edit + - if can?(current_user, :update_project_snippet, @snippet) + %li + = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do + Delete diff --git a/app/views/projects/snippets/_header_title.html.haml b/app/views/projects/snippets/_header_title.html.haml deleted file mode 100644 index 04f0bbe9853..00000000000 --- a/app/views/projects/snippets/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, "Snippets", namespace_project_snippets_path(@project.namespace, @project)) diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml index dc3ea1fcf12..216f70f5605 100644 --- a/app/views/projects/snippets/edit.html.haml +++ b/app/views/projects/snippets/edit.html.haml @@ -1,5 +1,4 @@ - page_title "Edit", @snippet.title, "Snippets" -= render "header_title" %h3.page-title Edit Snippet diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 103ff447464..96fee3b17b2 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,5 +1,4 @@ - page_title "Snippets" -= render "header_title" .row-content-block.top-block .pull-right diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml index e57237991b4..772a594269c 100644 --- a/app/views/projects/snippets/new.html.haml +++ b/app/views/projects/snippets/new.html.haml @@ -1,5 +1,4 @@ - page_title "New Snippets" -= render "header_title" %h3.page-title New Snippet diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 7c599563ce4..bae4d8f349f 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -1,18 +1,15 @@ - page_title @snippet.title, "Snippets" -= render "header_title" .snippet-holder = render 'shared/snippets/header' - %article.file-holder - .file-title + %article.file-holder.file-holder-no-border.snippet-file-content + .file-title.file-title-clear = blob_icon 0, @snippet.file_name - %strong - = @snippet.file_name + = @snippet.file_name .file-actions.hidden-xs = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" - = render 'shared/snippets/blob' %div#notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index 093d1d1bb0f..8a11dbfa9f4 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -1,7 +1,6 @@ -%span.btn-group.btn-grouped +%span.btn-group = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), class: 'btn btn-default', rel: 'nofollow' do - %i.fa.fa-download - %span source code + %span Source code %a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' } %span.caret %span.sr-only @@ -9,9 +8,7 @@ %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do - %i.fa.fa-download %span Download zip %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do - %i.fa.fa-download %span Download tar.gz diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index dbc35c16feb..844e1055810 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -15,11 +15,11 @@ = render 'projects/tags/download', ref: tag.name, project: @project - if can?(current_user, :push_code, @project) - = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn-grouped btn has-tooltip', title: "Edit release notes" do + = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes" do = icon("pencil") - if can?(current_user, :admin_project, @project) - = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do + = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do = icon("trash-o") - if commit diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml index ffeacb5a004..e4a78fadbeb 100644 --- a/app/views/projects/tags/destroy.js.haml +++ b/app/views/projects/tags/destroy.js.haml @@ -1,3 +1,2 @@ -$('.js-totaltags-count').html("#{@repository.tags.size}"); - if @repository.tags.empty? $('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index dc6ece30dd2..2779084fe38 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -1,29 +1,30 @@ +- @no_container = true - page_title "Tags" -= render "projects/commits/header_title" = render "projects/commits/head" -.row-content-block - - if can? current_user, :push_code, @project - .pull-right - = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do - = icon('plus') - New tag - .oneline - Tags give the ability to mark specific points in history as being important +%div{ class: (container_class) } + .top-area + .nav-text + Tags give the ability to mark specific points in history as being important -.tags - - unless @tags.empty? - %ul.content-list - - @tags.each do |tag| - = render 'tag', tag: @repository.find_tag(tag) + - if can? current_user, :push_code, @project + .nav-controls + = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do + New tag - = paginate @tags, theme: 'gitlab' + .tags + - unless @tags.empty? + %ul.content-list + - @tags.each do |tag| + = render 'tag', tag: @repository.find_tag(tag) - - else - .nothing-here-block - Repository has no tags yet. - %br - %small - Use git tag command to add a new one: + = paginate @tags, theme: 'gitlab' + + - else + .nothing-here-block + Repository has no tags yet. %br - %span.monospace git tag -a v1.4 -m 'version 1.4' + %small + Use git tag command to add a new one: + %br + %span.monospace git tag -a v1.4 -m 'version 1.4' diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index b40a6e5cb2d..3a097750d6e 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -1,5 +1,4 @@ - page_title "New Tag" -= render "projects/commits/header_title" - if @error .alert.alert-danger @@ -23,7 +22,7 @@ .form-group = label_tag :message, nil, class: 'control-label' .col-sm-10 - = text_field_tag :message, nil, required: false, tabindex: 3, class: 'form-control' + = text_area_tag :message, nil, required: false, tabindex: 3, class: 'form-control', rows: 5 .help-block Optionally, enter a message to create an annotated tag. %hr .form-group diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 9c916fd02de..b7d7d5c5382 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -1,5 +1,4 @@ - page_title @tag.name, "Tags" -= render "projects/commits/header_title" = render "projects/commits/head" .row-content-block @@ -19,15 +18,13 @@ %i.fa.fa-trash-o .title %span.item-title= @tag.name - - if @tag.message.present? - %span.light - - = strip_gpg_signature(@tag.message) - if @commit = render 'projects/branches/commit', commit: @commit, project: @project - else Cant find HEAD commit for this tag - + - if @tag.message.present? + %pre.body + = strip_gpg_signature(@tag.message) .append-bottom-default.prepend-top-default - if @release.description.present? diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 91fb2a44594..2abcfcdd7b2 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -1,17 +1,20 @@ +- @no_container = true + - page_title @path.presence || "Files", @ref -- header_title project_title(@project, "Files", project_files_path(@project)) = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") = render 'projects/last_push' += render "projects/commits/head" -.tree-controls - = render 'projects/find_file_link' - - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true +%div{ class: (container_class) } + .tree-controls + = render 'projects/find_file_link' + - if can? current_user, :download_code, @project + = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true -#tree-holder.tree-holder.clearfix - .nav-block - = render 'projects/tree/tree_header', tree: @tree + #tree-holder.tree-holder.clearfix + .nav-block + = render 'projects/tree/tree_header', tree: @tree - = render 'projects/tree/tree_content', tree: @tree + = render 'projects/tree/tree_content', tree: @tree diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index f91885b216d..7f3de47d7df 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -5,7 +5,7 @@ %h4.prepend-top-0 = page_title %p - Triggers can be used to force a rebuild of a specific branch or tag with an API call. + Triggers can force a specific branch or tag to rebuild with an API call. .col-lg-9 %h5.prepend-top-0 Your triggers @@ -18,8 +18,8 @@ %th = render partial: 'trigger', collection: @triggers, as: :trigger - else - %p.profile-settings-message.text-center.append-bottom-default - There are no triggers to use, add one by the button below. + %p.settings-message.text-center.append-bottom-default + No triggers have been created yet. Add one using the button below. = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f| = f.submit "Add Trigger", class: 'btn btn-success' @@ -28,8 +28,7 @@ Use CURL %p.light - Copy the token above and set your branch or tag name. This is the reference that will be rebuild. - + Copy the token above, set your branch or tag name, and that reference will be rebuilt. %pre :plain @@ -41,10 +40,10 @@ Use .gitlab-ci.yml %p.light - Copy the snippet to - %i .gitlab-ci.yml - of dependent project. - At the end of your build it will trigger this project to rebuilt. + In the + %code .gitlab-ci.yml + of the dependent project, include the following snippet. + The project will rebuild at the end of the build. %pre :plain @@ -57,9 +56,8 @@ %p.light Add - %strong variables[VARIABLE]=VALUE - to API request. - The value of variable could then be used to distinguish triggered build from normal one. + %code variables[VARIABLE]=VALUE + to an API request. Variable values can be used to distinguish between triggered builds and normal builds. %pre.append-bottom-0 :plain diff --git a/app/views/projects/variables/_content.html.haml b/app/views/projects/variables/_content.html.haml new file mode 100644 index 00000000000..0249e0c1bf1 --- /dev/null +++ b/app/views/projects/variables/_content.html.haml @@ -0,0 +1,8 @@ +%h4.prepend-top-0 + Secret Variables +%p + These variables will be set to environment by the runner. +%p + So you can use them for passwords, secret keys or whatever you want. +%p + The value of the variable can be visible in build log if explicitly asked to do so. diff --git a/app/views/projects/variables/_form.html.haml b/app/views/projects/variables/_form.html.haml new file mode 100644 index 00000000000..a5bae83e0ce --- /dev/null +++ b/app/views/projects/variables/_form.html.haml @@ -0,0 +1,10 @@ += form_for [@project.namespace.becomes(Namespace), @project, @variable] do |f| + = form_errors(@variable) + + .form-group + = f.label :key, "Key", class: "label-light" + = f.text_field :key, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true + .form-group + = f.label :value, "Value", class: "label-light" + = f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE", required: true + = f.submit btn_text, class: "btn btn-save" diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml new file mode 100644 index 00000000000..6c43f822db4 --- /dev/null +++ b/app/views/projects/variables/_table.html.haml @@ -0,0 +1,25 @@ +.table-responsive.variables-table + %table.table + %colgroup + %col + %col + %col{ width: 100 } + %thead + %th Key + %th Value + %th + %tbody + - @project.variables.each do |variable| + - if variable.id? + %tr + %td= variable.key + %td= variable.value + %td + = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do + %span.sr-only + Update + = icon("pencil") + = link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-delete", method: :delete, data: { confirm: "Are you sure?" } do + %span.sr-only + Remove + = icon("trash") diff --git a/app/views/projects/variables/index.html.haml b/app/views/projects/variables/index.html.haml new file mode 100644 index 00000000000..09bb54600af --- /dev/null +++ b/app/views/projects/variables/index.html.haml @@ -0,0 +1,17 @@ +- page_title "Variables" + +.row.prepend-top-default.append-bottom-default + .col-lg-3 + = render "content" + .col-lg-9 + %h5.prepend-top-0 + Add a variable + = render "form", btn_text: "Add new variable" + %hr + %h5.prepend-top-0 + Your variables (#{@project.variables.size}) + - if @project.variables.empty? + %p.settings-message.text-center.append-bottom-0 + No variables found, add one with the form above. + - else + = render "table" diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml index ca284b84d39..297a53ca98c 100644 --- a/app/views/projects/variables/show.html.haml +++ b/app/views/projects/variables/show.html.haml @@ -1,36 +1,9 @@ - page_title "Variables" -%h3.page-title - Secret Variables -%p.light - These variables will be set to environment by the runner. - %br - So you can use them for passwords, secret keys or whatever you want. - %br - The value of the variable can be visible in build log if explicitly asked to do so. - -%hr - - -= nested_form_for @project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f| - = form_errors(@project) - - = f.fields_for :variables do |variable_form| - .form-group - = variable_form.label :key, 'Key', class: 'control-label' - .col-sm-10 - = variable_form.text_field :key, class: 'form-control', placeholder: "PROJECT_VARIABLE" - - .form-group - = variable_form.label :value, 'Value', class: 'control-label' - .col-sm-10 - = variable_form.text_area :value, class: 'form-control', rows: 2, placeholder: "" - - = variable_form.link_to_remove "Remove this variable", class: 'btn btn-danger pull-right prepend-top-10' - %hr - %p - .clearfix - = f.link_to_add "Add a variable", :variables, class: 'btn btn-success pull-right' - - .form-actions - = f.submit 'Save changes', class: 'btn btn-save', return_to: request.original_url +.row.prepend-top-default.append-bottom-default + .col-lg-3 + = render "content" + .col-lg-9 + %h5.prepend-top-0 + Update variable + = render "form", btn_text: "Save variable" diff --git a/app/views/projects/wikis/_header_title.html.haml b/app/views/projects/wikis/_header_title.html.haml deleted file mode 100644 index 408adc36ca6..00000000000 --- a/app/views/projects/wikis/_header_title.html.haml +++ /dev/null @@ -1 +0,0 @@ -- header_title project_title(@project, 'Wiki', get_project_wiki_path(@project)) diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 2b91b7e8f65..4faa547769b 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -1,11 +1,9 @@ - if (@page && @page.persisted?) - = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do + = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do Page History - if can?(current_user, :create_wiki, @project) - = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do - %i.fa.fa-pencil-square-o + = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do Edit - if can?(current_user, :admin_wiki, @project) = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do - = icon('trash') Delete diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index a722fbc5352..988fe024e28 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -13,7 +13,6 @@ .nav-controls - if can?(current_user, :create_wiki, @project) = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do - = icon('plus') New Page = render 'projects/wikis/new' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 4dd818c7f67..cbd69ee1a73 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,9 +1,8 @@ - page_title "Edit", @page.title.capitalize, "Wiki" -= render "header_title" = render 'nav' .top-area - .nav-text + .nav-text.wiki-page %strong - if @page.persisted? = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page) diff --git a/app/views/projects/wikis/empty.html.haml b/app/views/projects/wikis/empty.html.haml index c7e490c3cd1..7dfa405d063 100644 --- a/app/views/projects/wikis/empty.html.haml +++ b/app/views/projects/wikis/empty.html.haml @@ -1,5 +1,4 @@ - page_title "Wiki" -= render "header_title" %h3.page-title Empty page %hr diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index ba3f2cadc48..ccceab6155e 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,5 +1,4 @@ - page_title "Git Access", "Wiki" -= render "header_title" = render 'nav' .row-content-block diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index dcaddae2b04..45460ed9f41 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,5 +1,4 @@ - page_title "History", @page.title.capitalize, "Wiki" -= render "header_title" = render 'nav' .top-area diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 92b494a513c..2f6162fa3c5 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -1,5 +1,4 @@ - page_title "Pages", "Wiki" -= render "header_title" = render 'nav' diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 067fb7f8f54..9166c0edb3b 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,5 +1,4 @@ - page_title @page.title.capitalize, "Wiki" -= render "header_title" = render 'nav' .top-area @@ -19,7 +18,7 @@ You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}. -.wiki-holder.prepend-top-default +.wiki-holder.prepend-top-default.append-bottom-default .wiki = preserve do = render_wiki_content(@page) diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml index 640890fbe92..8f68d6d1b87 100644 --- a/app/views/search/results/_issue.html.haml +++ b/app/views/search/results/_issue.html.haml @@ -7,7 +7,7 @@ - if issue.description.present? .description.term = preserve do - = search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project })) + = search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project, author: issue.author })) %span.light #{issue.project.name_with_namespace} - if issue.closed? diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 333f6533213..6331c2bd6b0 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -6,7 +6,7 @@ - if merge_request.description.present? .description.term = preserve do - = search_md_sanitize(markdown(merge_request.description, { project: merge_request.project })) + = search_md_sanitize(markdown(merge_request.description, { project: merge_request.project, author: merge_request.author })) %span.light #{merge_request.project.name_with_namespace} .pull-right diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index d9400b1d9fa..8163aff43b6 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -19,4 +19,4 @@ .note-search-result .term = preserve do - = search_md_sanitize(markdown(note.note, {no_header_anchors: true})) + = search_md_sanitize(markdown(note.note, {no_header_anchors: true, author: note.author})) diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 974751d9970..84b3f44c0ad 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -5,7 +5,7 @@ %a#clone-dropdown.clone-dropdown-btn.btn{href: '#', 'data-toggle' => 'dropdown'} %span = default_clone_protocol.upcase - = icon('angle-down') + = icon('caret-down') %ul.dropdown-menu.dropdown-menu-right.clone-options-dropdown %li = ssh_clone_button(project) diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index c38d9313dba..30055002213 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -1,5 +1,7 @@ -%ul.nav-links.event-filter +%ul.nav-links.event-filter.scrolling-tabs + .fade-left = event_filter_link EventFilter.push, 'Push events' = event_filter_link EventFilter.merged, 'Merge events' = event_filter_link EventFilter.comments, 'Comments' = event_filter_link EventFilter.team, 'Team' + .fade-right diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml index 8ff9d4c1c7f..a5df502d7b5 100644 --- a/app/views/shared/_issues.html.haml +++ b/app/views/shared/_issues.html.haml @@ -1,4 +1,4 @@ -- if @issues.any? +- if @issues.reorder(nil).any? - @issues.group_by(&:project).each do |group| .panel.panel-default.panel-small - project = group[0] diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index 9ce5562e667..478c04318c6 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -1,5 +1,13 @@ %span.label-row + - if can?(current_user, :admin_label, @project) + .js-toggle-priority.toggle-priority{ data: { url: remove_priority_namespace_project_label_path(@project.namespace, @project, label), + dom_id: dom_id(label) } } + %button.add-priority.btn.has-tooltip{ title: 'Prioritize', :'data-placement' => 'top' } + = icon('star-o') + %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', :'data-placement' => 'top' } + = icon('star') %span.label-name = link_to_label(label, tooltip: false) - %span.prepend-left-10 - = markdown(label.description, pipeline: :single_line)
\ No newline at end of file + - if label.description + %span.label-description + = markdown(label.description, pipeline: :single_line) diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml index dc89e36419c..87028ececd4 100644 --- a/app/views/shared/_labels_row.html.haml +++ b/app/views/shared/_labels_row.html.haml @@ -1,3 +1,10 @@ - labels.each do |label| - %span.label-row - = link_to_label(label, tooltip: false) + %span.label-row.btn-group{ role: "group", aria: { label: escape_once(label.name) }, style: "color: #{text_color_for_bg(label.color)}" } + = link_to namespace_project_label_path(@project.namespace, @project, label), + class: "btn btn-transparent has-tooltip", + style: "background-color: #{label.color};", + title: escape_once(label.description), + data: { container: "body" } do + = escape_once label.name + %button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } } + = icon("times") diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml index 1c58345278a..51622931e24 100644 --- a/app/views/shared/_new_project_item_select.html.haml +++ b/app/views/shared/_new_project_item_select.html.haml @@ -1,8 +1,7 @@ - if @projects.any? - .prepend-left-10.project-item-select-holder + .project-item-select-holder = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' } %a.btn.btn-new.new-project-item-select-button - = icon('plus') = local_assigns[:label] %b.caret diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml index d327bd0a96f..249bce926ce 100644 --- a/app/views/shared/_sort_dropdown.html.haml +++ b/app/views/shared/_sort_dropdown.html.haml @@ -6,8 +6,10 @@ - else = sort_title_recently_created %b.caret - %ul.dropdown-menu.dropdown-menu-align-right + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort %li + = link_to page_filter_path(sort: sort_value_priority) do + = sort_title_priority = link_to page_filter_path(sort: sort_value_recently_created) do = sort_title_recently_created = link_to page_filter_path(sort: sort_value_oldest_created) do diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index 40c6eb9be45..a25365a94f2 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -6,10 +6,10 @@ - if group_member .controls.hidden-xs - if can?(current_user, :admin_group, group) - = link_to edit_group_path(group), class: "btn-sm btn btn-grouped" do - %i.fa.fa-cogs + = link_to edit_group_path(group), class: "btn" do + = icon('cogs') - = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Leave this group' do + = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn", title: 'Leave this group' do = icon('sign-out') .stats diff --git a/app/views/shared/groups/_list.html.haml b/app/views/shared/groups/_list.html.haml index 1aa7ed1f2eb..427595c47a5 100644 --- a/app/views/shared/groups/_list.html.haml +++ b/app/views/shared/groups/_list.html.haml @@ -3,4 +3,4 @@ - groups.each_with_index do |group, i| = render "shared/groups/group", group: group - else - %h3 No groups found + .nothing-here-block No groups found diff --git a/app/views/shared/icons/_activity.svg b/app/views/shared/icons/_activity.svg new file mode 100644 index 00000000000..d465504b154 --- /dev/null +++ b/app/views/shared/icons/_activity.svg @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch --> + <title>path-1</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="_activity" fill="#7E7D7D"> + <g id="Page-1"> + <g id="path-1"> + <path d="M5,0 C4.448,0 4,0.448 4,1 L4,3 L1,3 C0.448,3 0,3.448 0,4 L0,9 C0,9.552 0.448,10 1,10 L5,10 L5,8 L11,8 L11,10 L15,10 C15.552,10 16,9.552 16,9 L16,4 C16,3.448 15.552,3 15,3 L12,3 L12,1 C12,0.448 11.552,0 11,0 L5,0 L5,0 L5,0 L5,0 Z M6,2.5 C6,2.224 6.224,2 6.5,2 L9.5,2 C9.776,2 10,2.224 10,2.5 C10,2.776 9.776,3 9.5,3 L6.5,3 C6.224,3 6,2.776 6,2.5 L6,2.5 L6,2.5 L6,2.5 Z M6,11 L10.001,11 L10.001,9 L6,9 L6,11 L6,11 L6,11 L6,11 Z M11,11 L11,12 L5,12 L5,11 L1,11 C0.448,11 0,11.448 0,12 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,12 C16,11.448 15.552,11 15,11 L11,11 L11,11 L11,11 L11,11 Z"></path> + </g> + </g> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_commits.svg b/app/views/shared/icons/_commits.svg new file mode 100644 index 00000000000..ba9bb89935e --- /dev/null +++ b/app/views/shared/icons/_commits.svg @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch --> + <title>Pasted Image 240</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <path d="M3,8 C3,5.951 4.236,4.194 6,3.422 L6,0 L1,0 C0.448,0 0,0.448 0,1 L0,15 C0,15.552 0.448,16 1,16 L6,16 L6,12.578 C4.236,11.806 3,10.049 3,8 M7,12.899 L7,16 L9,16 L9,12.899 C8.677,12.965 8.343,13 8,13 C7.657,13 7.323,12.965 7,12.899 M15,0 L10,0 L10,3.422 C11.764,4.194 13,5.951 13,8 C13,10.049 11.764,11.806 10,12.578 L10,16 L15,16 C15.552,16 16,15.552 16,15 L16,1 C16,0.448 15.552,0 15,0 M10,8 C10,9.105 9.105,10 8,10 C6.895,10 6,9.105 6,8 C6,6.895 6.895,6 8,6 C9.105,6 10,6.895 10,8 M4,8 C4,10.209 5.791,12 8,12 C10.209,12 12,10.209 12,8 C12,5.791 10.209,4 8,4 C5.791,4 4,5.791 4,8 M9,3.101 L9,0 L7,0 L7,3.101 C7.323,3.035 7.657,3 8,3 C8.343,3 8.677,3.035 9,3.101" id="Pasted-Image-240" fill="#7E7D7D"></path> + </g> +</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_contributionanalytics.svg b/app/views/shared/icons/_contributionanalytics.svg new file mode 100644 index 00000000000..adf09a14964 --- /dev/null +++ b/app/views/shared/icons/_contributionanalytics.svg @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch --> + <title>Group</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="Group"> + <path d="M8,0 C3.581,0 0,3.581 0,8 C0,12.419 3.581,16 8,16 C12.419,16 16,12.419 16,8 C16,3.581 12.419,0 8,0 M8,2 C11.308,2 14,4.692 14,8 C14,11.308 11.308,14 8,14 C4.692,14 2,11.308 2,8 C2,4.692 4.692,2 8,2" id="Fill-1" fill="#7E7C7C"></path> + <polygon id="Stroke-6" fill="#7E7C7C" points="2.0197351 9.86809696 6.4567351 6.52409696 5.79233671 6.46815759 9.53233671 10.4271576 9.87070552 10.78534 10.2338016 10.4522494 15.0258016 6.05624938 14.3497984 5.31935062 9.55779844 9.71535062 10.2592633 9.74044241 6.51926329 5.78144241 6.21208651 5.45627854 5.8548649 5.72550304 1.4178649 9.06950304"></polygon> + <path d="M7.0313,6.3928 C7.0313,6.9448 6.5833,7.3928 6.0313,7.3928 C5.4793,7.3928 5.0313,6.9448 5.0313,6.3928 C5.0313,5.8408 5.4793,5.3928 6.0313,5.3928 C6.5833,5.3928 7.0313,5.8408 7.0313,6.3928" id="Fill-8" fill="#FEFEFE"></path> + <path d="M6.5313,6.3928 C6.5313,6.66865763 6.30715763,6.8928 6.0313,6.8928 C5.75544237,6.8928 5.5313,6.66865763 5.5313,6.3928 C5.5313,6.11694237 5.75544237,5.8928 6.0313,5.8928 C6.30715763,5.8928 6.5313,6.11694237 6.5313,6.3928 L6.5313,6.3928 Z M7.5313,6.3928 C7.5313,5.56465763 6.85944237,4.8928 6.0313,4.8928 C5.20315763,4.8928 4.5313,5.56465763 4.5313,6.3928 C4.5313,7.22094237 5.20315763,7.8928 6.0313,7.8928 C6.85944237,7.8928 7.5313,7.22094237 7.5313,6.3928 L7.5313,6.3928 Z" id="Stroke-10" fill="#7E7C7C"></path> + <path d="M10.8854,9.8715 C10.8854,10.4235 10.4374,10.8715 9.8854,10.8715 C9.3334,10.8715 8.8854,10.4235 8.8854,9.8715 C8.8854,9.3195 9.3334,8.8715 9.8854,8.8715 C10.4374,8.8715 10.8854,9.3195 10.8854,9.8715" id="Fill-12" fill="#FEFEFE"></path> + <path d="M10.3854,9.8715 C10.3854,10.1473576 10.1612576,10.3715 9.8854,10.3715 C9.60954237,10.3715 9.3854,10.1473576 9.3854,9.8715 C9.3854,9.59564237 9.60954237,9.3715 9.8854,9.3715 C10.1612576,9.3715 10.3854,9.59564237 10.3854,9.8715 L10.3854,9.8715 Z M11.3854,9.8715 C11.3854,9.04335763 10.7135424,8.3715 9.8854,8.3715 C9.05725763,8.3715 8.3854,9.04335763 8.3854,9.8715 C8.3854,10.6996424 9.05725763,11.3715 9.8854,11.3715 C10.7135424,11.3715 11.3854,10.6996424 11.3854,9.8715 L11.3854,9.8715 Z" id="Stroke-14" fill="#7E7C7C"></path> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_files.svg b/app/views/shared/icons/_files.svg new file mode 100644 index 00000000000..fc378d81e40 --- /dev/null +++ b/app/views/shared/icons/_files.svg @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch --> + <title>Pasted Image 237</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="Pasted-Image-237"> + <path d="M15.1111,16 C15.6021,16 16.0001,15.602 16.0001,15.111 L16.0001,4.444 C15.5341,3.983 12.0671,0.378 11.5551,0 L0.8891,0 C0.3981,0 0.0001,0.398 0.0001,0.889 L0.0001,15.111 C0.0001,15.602 0.3981,16 0.8891,16 L15.1111,16 M14.0001,14.111 L1.8891,14.111 L1.8891,2 L10.8131,2 C11.4451,2.42 13.5811,4.555 14.0001,5.187 L14.0001,14.111" id="Fill-1" fill="#7E7D7D"></path> + <path d="M0.889,0 C0.398,0 0,0.398 0,0.889 L0,15.111 C0,15.602 0.398,16 0.889,16 L15.111,16 C15.602,16 16,15.602 16,15.111 L16,4.445 C15.534,3.983 12.068,0.377 11.555,0 L0.889,0 L0.889,0 Z M1.889,2 L10.813,2 C11.446,2.42 13.581,4.554 14,5.187 L14,14.111 L1.889,14.111 L1.889,2 L1.889,2 Z" id="Clip-4"></path> + <polygon id="Fill-6" fill="#7E7D7D" points="9 7 11 7 11 2 9 2"></polygon> + <polygon id="Clip-9" points="9 7 11 7 11 2.001 9 2.001"></polygon> + <polygon id="Fill-11" fill="#7E7D7D" points="10 7 15.444 7 15.444 5 10 5"></polygon> + <polygon id="Clip-14" points="10 7 15.444 7 15.444 5 10 5"></polygon> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_group.svg b/app/views/shared/icons/_group.svg new file mode 100644 index 00000000000..75cae0d16c8 --- /dev/null +++ b/app/views/shared/icons/_group.svg @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch --> + <title>Group</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="Group" fill="#303030"> + <path d="M15.6667,10.0105 L10.3337,10.0105 C10.1497,10.0105 9.9997,10.1775 9.9997,10.3845 L9.9997,15.6145 C9.9997,15.8215 10.1497,15.9885 10.3337,15.9885 L15.6667,15.9885 C15.8507,15.9885 15.9997,15.8215 15.9997,15.6145 L15.9997,10.3845 C15.9997,10.1775 15.8507,10.0105 15.6667,10.0105 L15.6667,10.0105 L15.6667,10.0105 Z M11.9997,14.0105 L13.9997,14.0105 L13.9997,12.0105 L11.9997,12.0105 L11.9997,14.0105 L11.9997,14.0105 Z" id="Fill-11"></path> + <path d="M5.6667,10.0105 L0.3337,10.0105 C0.1497,10.0105 -0.0003,10.1775 -0.0003,10.3845 L-0.0003,15.6145 C-0.0003,15.8215 0.1497,15.9885 0.3337,15.9885 L5.6667,15.9885 C5.8507,15.9885 5.9997,15.8215 5.9997,15.6145 L5.9997,10.3845 C5.9997,10.1775 5.8507,10.0105 5.6667,10.0105 L5.6667,10.0105 L5.6667,10.0105 Z M1.9997,14.0105 L3.9997,14.0105 L3.9997,12.0105 L1.9997,12.0105 L1.9997,14.0105 L1.9997,14.0105 Z" id="Fill-8"></path> + <polygon id="Stroke-1" points="12.5 7.5834 3.5 7.5834 3.5 9.5834 12.5 9.5834"></polygon> + <polygon id="Stroke-3" points="9 9.0834 9 5.0834 7 5.0834 7 9.0834"></polygon> + <polygon id="Stroke-4" points="4 11.0834 4 7.5834 2 7.5834 2 11.0834"></polygon> + <polygon id="Stroke-6" points="14 11.0834 14 7.5834 12 7.5834 12 11.0834"></polygon> + <path d="M11.6667,6.21724894e-15 L4.3337,6.21724894e-15 C4.1497,6.21724894e-15 3.9997,0.167 3.9997,0.374 L3.9997,6.604 C3.9997,6.811 4.1497,6.978 4.3337,6.978 L11.6667,6.978 C11.8507,6.978 11.9997,6.811 11.9997,6.604 L11.9997,0.374 C11.9997,0.167 11.8507,6.21724894e-15 11.6667,6.21724894e-15 L11.6667,6.21724894e-15 L11.6667,6.21724894e-15 Z M5.9997,5 L9.9997,5 L9.9997,2 L5.9997,2 L5.9997,5 L5.9997,5 Z" id="Fill-14"></path> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_issues.svg b/app/views/shared/icons/_issues.svg new file mode 100644 index 00000000000..2682c27ade9 --- /dev/null +++ b/app/views/shared/icons/_issues.svg @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch --> + <title>Group</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="Group" fill="#7E7C7C"> + <path d="M8,0 C3.581,0 0,3.581 0,8 C0,12.419 3.581,16 8,16 C12.419,16 16,12.419 16,8 C16,3.581 12.419,0 8,0 M8,2 C11.308,2 14,4.692 14,8 C14,11.308 11.308,14 8,14 C4.692,14 2,11.308 2,8 C2,4.692 4.692,2 8,2" id="Fill-1"></path> + <path d="M7.1597,4 L8.8887,4 L8.8887,8 L7.1107,8 L7.1597,4 Z M7.1597,9.6667 L8.8887,9.6667 L8.8887,11.4447 L7.1107,11.4447 L7.1597,9.6667 Z" id="Combined-Shape"></path> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_members.svg b/app/views/shared/icons/_members.svg new file mode 100644 index 00000000000..f8043b31fe8 --- /dev/null +++ b/app/views/shared/icons/_members.svg @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="22px" height="16px" viewBox="0 0 22 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch --> + <title>Group</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="Group" fill="#7E7C7C"> + <path d="M6.4357,11.8588 C7.1487,11.2798 7.8797,10.7808 8.5357,10.3708 C8.5837,10.3008 8.6187,10.2338 8.6187,10.1768 L8.6187,8.8088 C8.9197,8.5218 9.0927,8.1248 9.0927,7.7028 L9.0927,5.3748 C9.0927,3.9478 7.9187,2.7858 6.4757,2.7858 L5.9687,2.7858 C4.5247,2.7858 3.3507,3.9478 3.3507,5.3748 L3.3507,7.7028 C3.3507,8.1248 3.5247,8.5218 3.8247,8.8088 L3.8247,10.5838 C3.2537,10.8738 1.8797,11.6198 0.5967,12.6618 C0.2177,12.9698 -0.0003,13.4258 -0.0003,13.9138 L-0.0003,15.5088 C-0.0003,15.5438 0.0857,15.7668 0.3467,15.7778 C1.3257,15.8198 3.8417,15.8328 5.9617,15.9038 C5.8337,15.8148 5.7447,15.6748 5.7447,15.5088 L5.7447,13.5498 C5.7447,12.9848 5.9967,12.2158 6.4357,11.8588" id="Fill-1"></path> + <path d="M21.3092,12.1 C19.6932,10.787 17.9592,9.86 17.3042,9.53 L17.3042,7.235 C17.6722,6.9 17.8862,6.428 17.8862,5.925 L17.8862,3.066 C17.8862,1.376 16.4952,0 14.7852,0 L14.1632,0 C12.4532,0 11.0622,1.376 11.0622,3.066 L11.0622,5.925 C11.0622,6.428 11.2752,6.9 11.6442,7.235 L11.6442,9.53 C10.9892,9.86 9.2542,10.787 7.6392,12.1 C7.2002,12.457 6.9482,12.985 6.9482,13.55 L6.9482,15.509 C6.9482,15.78 7.1702,16 7.4442,16 L14.1172,16 L14.1172,11.704 C12.6812,11.595 11.5652,10.853 11.5652,9.945 C11.5652,9.804 11.5982,9.669 11.6482,9.538 C11.9502,10.326 13.0982,10.913 14.4762,10.913 C15.8532,10.913 17.0012,10.326 17.3032,9.538 C17.3532,9.669 17.3862,9.804 17.3862,9.945 C17.3862,10.793 16.4152,11.5 15.1172,11.679 L15.1172,16 L21.5032,16 C21.7772,16 22.0002,15.78 22.0002,15.509 L22.0002,13.55 C22.0002,12.985 21.7482,12.457 21.3092,12.1" id="Fill-4"></path> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_milestones.svg b/app/views/shared/icons/_milestones.svg new file mode 100644 index 00000000000..3d62ecc0631 --- /dev/null +++ b/app/views/shared/icons/_milestones.svg @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="16px" height="17px" viewBox="0 0 16 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch --> + <title>Group</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="Group" fill="#7E7C7C"> + <path d="M15.1111,1 L0.8891,1 C0.3981,1 0.0001,1.446 0.0001,1.996 L0.0001,15.945 C0.0001,16.495 0.3981,16.941 0.8891,16.941 L15.1111,16.941 C15.6021,16.941 16.0001,16.495 16.0001,15.945 L16.0001,1.996 C16.0001,1.446 15.6021,1 15.1111,1 L15.1111,1 L15.1111,1 Z M14.0001,6.0002 L14.0001,14.949 L2.0001,14.949 L2.0001,6.0002 L14.0001,6.0002 Z M14.0001,4.0002 L14.0001,2.993 L2.0001,2.993 L2.0001,4.0002 L14.0001,4.0002 Z" id="Combined-Shape"></path> + <polygon id="Fill-11" points="3 2.0002 5 2.0002 5 0.0002 3 0.0002"></polygon> + <polygon id="Fill-16" points="11 2.0002 13 2.0002 13 0.0002 11 0.0002"></polygon> + <path d="M5.37709616,11.5511984 L6.92309616,12.7821984 C7.35112915,13.123019 7.97359761,13.0565604 8.32002627,12.6330535 L10.7740263,9.63305349 C11.1237073,9.20557058 11.0606364,8.57555475 10.6331535,8.22587373 C10.2056706,7.87619272 9.57565475,7.93926361 9.22597373,8.36674651 L6.77197373,11.3667465 L8.16890384,11.2176016 L6.62290384,9.98660159 C6.19085236,9.6425813 5.56172188,9.71394467 5.21770159,10.1459962 C4.8736813,10.5780476 4.94504467,11.2071781 5.37709616,11.5511984 L5.37709616,11.5511984 Z" id="Stroke-21"></path> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_mr.svg b/app/views/shared/icons/_mr.svg new file mode 100644 index 00000000000..dd3dbcc4473 --- /dev/null +++ b/app/views/shared/icons/_mr.svg @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch --> + <title>Group</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="Group" fill="#7E7C7C"> + <path d="M15.1111,0 L0.8891,0 C0.3981,0 0.0001,0.446 0.0001,0.996 L0.0001,14.945 C0.0001,15.495 0.3981,15.941 0.8891,15.941 L15.1111,15.941 C15.6021,15.941 16.0001,15.495 16.0001,14.945 L16.0001,0.996 C16.0001,0.446 15.6021,0 15.1111,0 L15.1111,0 L15.1111,0 Z M2.0001,13.949 L14.0001,13.949 L14.0001,1.993 L2.0001,1.993 L2.0001,13.949 Z M2,5.0002 L14,5.0002 L14,3.0002 L2,3.0002 L2,5.0002 Z" id="Combined-Shape"></path> + <path d="M8.547,12.0002 L12,12.0002 L12,10.0002 L8.547,10.0002 L8.547,12.0002 Z M5.2029,12 L3.9999,10.867 L5.2029,9.501 L3.9999,8.181 L5.2029,7 L7.4529,9.499 L5.2029,12 Z" id="Combined-Shape"></path> + </g> + </g> +</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_pipelines.svg b/app/views/shared/icons/_pipelines.svg new file mode 100644 index 00000000000..794e8a27025 --- /dev/null +++ b/app/views/shared/icons/_pipelines.svg @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch --> + <title>Pasted Image 246</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <path d="M12.5,14 C11.672,14 11,13.328 11,12.5 C11,11.672 11.672,11 12.5,11 C13.328,11 14,11.672 14,12.5 C14,13.328 13.328,14 12.5,14 M12.5,9 L3.5,9 C1.567,9 0,10.567 0,12.5 C0,14.433 1.567,16 3.5,16 L12.5,16 C14.433,16 16,14.433 16,12.5 C16,10.567 14.433,9 12.5,9 M3.5,2 C4.328,2 5,2.672 5,3.5 C5,4.328 4.328,5 3.5,5 C2.672,5 2,4.328 2,3.5 C2,2.672 2.672,2 3.5,2 M3.5,7 L12.5,7 C14.433,7 16,5.433 16,3.5 C16,1.567 14.433,0 12.5,0 L3.5,0 C1.567,0 0,1.567 0,3.5 C0,5.433 1.567,7 3.5,7" id="Pasted-Image-246" fill="#303030"></path> + </g> +</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_project.svg b/app/views/shared/icons/_project.svg new file mode 100644 index 00000000000..1e8b43f8c6b --- /dev/null +++ b/app/views/shared/icons/_project.svg @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch --> + <title>Page 1</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <path d="M6,6 L12,6 L12,5 L6,5 L6,6 Z M6,8 L12,8 L12,7 L6,7 L6,8 Z M6,10 L12,10 L12,9 L6,9 L6,10 Z M6,12 L12,12 L12,11 L6,11 L6,12 Z M4,6 L5,6 L5,5 L4,5 L4,6 Z M4,8 L5,8 L5,7 L4,7 L4,8 Z M4,10 L5,10 L5,9 L4,9 L4,10 Z M4,12 L5,12 L5,11 L4,11 L4,12 Z M13,3 L10,3 L10,4 L6,4 L6,3 L3,3 L3,13 L13,13 L13,3 Z M2,14 L14,14 L14,2 L2,2 L2,14 Z M1,0 C0.448,0 0,0.448 0,1 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,1 C16,0.448 15.552,0 15,0 L1,0 Z" fill="#7F7E7E"></path> + </g> +</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_wiki.svg b/app/views/shared/icons/_wiki.svg new file mode 100644 index 00000000000..182d91e23aa --- /dev/null +++ b/app/views/shared/icons/_wiki.svg @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch --> + <title>Pasted Image 241</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <path d="M2.004,12.9999459 L3.939,12.9999459 L3.939,4.99994585 L2.004,4.99994585 L2.004,12.9999459 Z M7.017,9.99994585 L13.018,9.99994585 L13.018,8.99994585 L7.017,8.99994585 L7.017,9.99994585 Z M7.017,7.99994585 L13.018,7.99994585 L13.018,6.99994585 L7.017,6.99994585 L7.017,7.99994585 Z M7.017,5.99994585 L13.018,5.99994585 L13.018,4.99994585 L7.017,4.99994585 L7.017,5.99994585 Z M14.754,-5.41499267e-05 L4.938,-5.41499267e-05 C4.386,-5.41499267e-05 3.938,0.44794585 3.938,0.99994585 L3.938,2.99994585 L1,2.99994585 C0.448,2.99994585 0,3.44794585 0,3.99994585 L0,12.9999459 C0.037,13.4999459 -0.25,16.0509459 3.938,15.9999459 L12.408,15.9999459 C12.408,15.9999459 15.754,15.9169459 15.754,13.9999459 L15.754,0.99994585 C15.754,0.44794585 15.306,-5.41499267e-05 14.754,-5.41499267e-05 L14.754,-5.41499267e-05 Z" id="Pasted-Image-241" fill="#7E7D7D"></path> + </g> +</svg>
\ No newline at end of file diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 9474462cbd1..380ab465bf4 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,6 +1,8 @@ .issues-filters .issues-details-filters.row-content-block.second-block - = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name]), method: :get, class: 'filter-form' do + = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search]), method: :get, class: 'filter-form js-filter-form' do + - if params[:issue_search].present? + = hidden_field_tag :issue_search, params[:issue_search] - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project) .check-all-holder = check_box_tag "check_all_issues", nil, false, @@ -10,7 +12,7 @@ - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit", - placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) + placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author], field_name: "author_id", default_label: "Author" } }) .filter-item.inline - if params[:assignee_id].present? @@ -29,7 +31,7 @@ - if controller.controller_name == 'issues' .issues_bulk_update.hide - = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do + = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post, class: 'bulk-update' do .filter-item.inline = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do %ul @@ -42,6 +44,10 @@ placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) .filter-item.inline = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + + .filter-item.inline.labels-filter + = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, show_footer: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } + = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] .filter-item.inline diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 5c52cc6d1da..17e2a7e9290 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -44,45 +44,53 @@ This issue is confidential and should only be visible to team members - if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) + - has_due_date = issuable.has_attribute?(:due_date) %hr - .form-group - .issue-assignee - = f.label :assignee_id, "Assignee", class: 'control-label' - .col-sm-10 - .issuable-form-select-holder - = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", - placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, - selected: issuable.assignee_id, project: @target_project || @project, - first_user: true, current_user: true, include_blank: true) - - = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' - .form-group - .issue-milestone - = f.label :milestone_id, "Milestone", class: 'control-label' - .col-sm-10 - - if milestone_options(issuable).present? + .row + %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } + .form-group.issue-assignee + = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" + .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder - = f.select(:milestone_id, milestone_options(issuable), - { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } }) - - else - .prepend-top-10 - %span.light No open milestones available. - - - if can? current_user, :admin_milestone, issuable.project - = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank - .form-group - - has_labels = issuable.project.labels.any? - = f.label :label_ids, "Labels", class: 'control-label' - .col-sm-10{ class: ('issuable-form-padding-top' if !has_labels) } - - if has_labels - .issuable-form-select-holder - = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, - { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } - - else - %span.light No labels yet. - - - if can? current_user, :admin_label, issuable.project - = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank + = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", + placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, + selected: issuable.assignee_id, project: @target_project || @project, + first_user: true, current_user: true, include_blank: true) + %div + = link_to 'Assign to me', '#', class: 'assign-to-me-link prepend-top-5 inline' + .form-group.issue-milestone + = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" + .col-sm-10{ class: ("col-lg-8" if has_due_date) } + - if milestone_options(issuable).present? + .issuable-form-select-holder + = f.select(:milestone_id, milestone_options(issuable), + { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } }) + - else + .prepend-top-10 + %span.light No open milestones available. + - if can? current_user, :admin_milestone, issuable.project + %div + = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" + .form-group + - has_labels = issuable.project.labels.any? + = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" + .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } + - if has_labels + .issuable-form-select-holder + = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, + { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } + - else + %span.light No labels yet. + - if can? current_user, :admin_label, issuable.project + %div + = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" + - if has_due_date + .col-lg-6 + .form-group + = f.label :due_date, "Due date", class: "control-label" + .col-sm-10 + .issuable-form-select-holder + = f.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" - if issuable.can_move?(current_user) %hr @@ -90,9 +98,7 @@ = label_tag :move_to_project_id, 'Move', class: 'control-label' .col-sm-10 .issuable-form-select-holder - - projects = project_options(issuable, current_user, ability: :admin_issue) - = select_tag(:move_to_project_id, projects, include_blank: true, - class: 'select2', data: { placeholder: 'Select project' }) + = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id) } %span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default', title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' } @@ -114,6 +120,13 @@ - if @merge_request.new_record? = link_to 'Change branches', mr_change_branches_path(@merge_request) + - if @merge_request.can_remove_source_branch?(current_user) + .form-group + .col-sm-10.col-sm-offset-2 + .checkbox + = label_tag 'merge_request[force_remove_source_branch]' do + = check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch? + Remove source branch when merge request is accepted. - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) .row-content-block{class: (is_footer ? "footer-block" : "middle-block")} diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 61fd1e9c335..d34d28f6736 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,14 +1,25 @@ +- show_create = local_assigns.fetch(:show_create, true) +- extra_options = local_assigns.fetch(:extra_options, true) +- filter_submit = local_assigns.fetch(:filter_submit, true) +- show_footer = local_assigns.fetch(:show_footer, true) +- data_options = local_assigns.fetch(:data_options, {}) +- classes = local_assigns.fetch(:classes, []) +- dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"} +- dropdown_data.merge!(data_options) +- classes << 'js-extra-options' if extra_options +- classes << 'js-filter-submit' if filter_submit + - if params[:label_name].present? - if params[:label_name].respond_to?('any?') - params[:label_name].each do |label| = hidden_field_tag "label_name[]", label, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-multiselect.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name[]", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} + %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data} %span.dropdown-toggle-text = h(multi_label_name(params[:label_name], "Label")) = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label" } - - if can? current_user, :admin_label, @project and @project + = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer, show_create: show_create } + - if show_create and @project and can?(current_user, :admin_label, @project) = render partial: "shared/issuable/label_page_create" = dropdown_loading diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml index 7f4867417f7..0acb8253139 100644 --- a/app/views/shared/issuable/_label_page_default.html.haml +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -1,20 +1,22 @@ - title = local_assigns.fetch(:title, 'Assign labels') +- show_create = local_assigns.fetch(:show_create, true) +- show_footer = local_assigns.fetch(:show_footer, true) - filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search labels') .dropdown-page-one = dropdown_title(title) - = dropdown_filter(filter_placeholder) + = dropdown_filter(filter_placeholder, search_id: "label-name") = dropdown_content - - if @project + - if @project && show_footer = dropdown_footer do %ul.dropdown-footer-list - - if can? current_user, :admin_label, @project + - if can?(current_user, :admin_label, @project) %li %a.dropdown-toggle-page{href: "#"} Create new %li = link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do - - if can? current_user, :admin_label, @project + - if show_create && @project && can?(current_user, :admin_label, @project) Manage labels - else View labels - = dropdown_loading
\ No newline at end of file + = dropdown_loading diff --git a/app/views/shared/issuable/_search_form.html.haml b/app/views/shared/issuable/_search_form.html.haml index afad48499b7..186963b32b8 100644 --- a/app/views/shared/issuable/_search_form.html.haml +++ b/app/views/shared/issuable/_search_form.html.haml @@ -1,8 +1,2 @@ = form_tag(path, method: :get, id: "issue_search_form", class: 'issue-search-form') do = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by name ...', class: 'form-control issue_search search-text-input input-short', spellcheck: false } - = hidden_field_tag :state, params['state'] - = hidden_field_tag :scope, params['scope'] - = hidden_field_tag :assignee_id, params['assignee_id'] - = hidden_field_tag :author_id, params['author_id'] - = hidden_field_tag :milestone_id, params['milestone_id'] - = hidden_field_tag :label_id, params['label_id'] diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index ed1b8a8da2a..fb906de829a 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -2,23 +2,8 @@ .issuable-sidebar - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .block.issuable-sidebar-header - %span.issuable-count.hide-collapsed.pull-left - = issuable.iid - of - = issuables_count(issuable) %a.gutter-toggle.pull-right.js-sidebar-toggle{href: '#'} = sidebar_gutter_toggle_icon - .issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'} - - if prev_issuable = prev_issuable_for(issuable) - = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn issuable-pager' - - else - %a.btn.btn-default.issuable-pager.disabled{href: '#'} - Prev - - if next_issuable = next_issuable_for(issuable) - = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn issuable-pager' - - else - %a.btn.btn-default.issuable-pager.disabled{href: '#'} - Next = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| .block.assignee @@ -56,7 +41,8 @@ = icon('clock-o') %span - if issuable.milestone - = issuable.milestone.title + %span.has-tooltip{title: milestone_remaining_days(issuable.milestone), data: {container: 'body', html: 1, placement: 'left'}} + = issuable.milestone.title - else None .title.hide-collapsed @@ -67,7 +53,8 @@ .value.bold.hide-collapsed - if issuable.milestone = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do - = issuable.milestone.title + %span.has-tooltip{title: milestone_remaining_days(issuable.milestone), data: {container: 'body', html: 1}} + = issuable.milestone.title - else .light None @@ -87,10 +74,16 @@ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = link_to 'Edit', '#', class: 'edit-link pull-right' .value.bold.hide-collapsed - - if issuable.due_date - = issuable.due_date.to_s(:medium) - - else - .light None + %span.value-content + - if issuable.due_date + = issuable.due_date.to_s(:medium) + - else + None + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + %span.light.js-remove-due-date-holder{ class: ("hidden" if issuable.due_date.nil?) } + \- + %a.js-remove-due-date{ href: "#", role: "button" } + remove due date - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .selectbox.hide-collapsed = f.hidden_field :due_date, value: issuable.due_date @@ -108,20 +101,20 @@ .sidebar-collapsed-icon = icon('tags') %span - = issuable.labels.count + = issuable.labels_array.size .title.hide-collapsed Labels = icon('spinner spin', class: 'block-loading') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) } - - if issuable.labels.any? - - issuable.labels.each do |label| + .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) } + - if issuable.labels_array.any? + - issuable.labels_array.each do |label| = link_to_label(label, type: issuable.to_ability_name) - else .light None .selectbox.hide-collapsed - - issuable.labels.each do |label| + - issuable.labels_array.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} @@ -142,7 +135,7 @@ .title.hide-collapsed Notifications - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' - %button.btn.btn-block.btn-gray.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } + %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } %span= subscribed ? 'Unsubscribe' : 'Subscribe' .subscription-status.hide-collapsed{data: {status: subscribtion_status}} .unsubscribed{class: ( 'hidden' if subscribed )} diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 6b25745c554..acc3ccf4dcf 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -35,11 +35,9 @@ .col-sm-6= render('shared/milestone_expired', milestone: milestone) .col-sm-6 - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? - = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs" do - = icon('pencil-square-o') + = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs btn-grouped" do Edit \ - = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close" - = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do - = icon('trash-o') + = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped" + = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove btn-grouped" do Delete diff --git a/app/views/shared/milestones/_participants_tab.html.haml b/app/views/shared/milestones/_participants_tab.html.haml index 67ae85ac276..549d2e2f61e 100644 --- a/app/views/shared/milestones/_participants_tab.html.haml +++ b/app/views/shared/milestones/_participants_tab.html.haml @@ -3,6 +3,6 @@ %li = link_to user, title: user.name, class: "darken" do = image_tag avatar_icon(user, 32), class: "avatar s32" - %strong= truncate(user.name, lenght: 40) + %strong= truncate(user.name, length: 40) %br %small.cgray= user.username diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index ab8b022411d..b8b66d08db8 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -12,12 +12,9 @@ %li.project-row{ class: css_class } = cache(cache_key) do .controls - - if project.main_language - %span - = project.main_language - if project.commit.try(:status) %span - = render_ci_status(project.commit) + = render_commit_status(project.commit) - if forks %span = icon('code-fork') diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index e65b1814872..af753496260 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -1,25 +1,24 @@ -.detail-page-header - .snippet-box.has-tooltip{class: visibility_level_color(@snippet.visibility_level), title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: 'body' }} +.detail-page-header.clearfix + .snippet-box.has-tooltip.inline.append-right-5{ title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: "body" } } + %span.sr-only + = visibility_level_label(@snippet.visibility_level) = visibility_level_icon(@snippet.visibility_level, fw: false) - = visibility_level_label(@snippet.visibility_level) - %span.identifier - Snippet ##{@snippet.id} + %strong.item-title + Snippet #{@snippet.to_reference} %span.creator - · created by #{link_to_member(@project, @snippet.author, size: 24)} - · + created by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title")} = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago') - if @snippet.updated_at != @snippet.created_at %span - · = icon('edit', title: 'edited') = time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago') - .pull-right + .snippet-actions - if @snippet.project_id? = render "projects/snippets/actions" - else = render "snippets/actions" -.detail-page-description.row-content-block.second-block - %h2.title - = markdown escape_once(@snippet.title), pipeline: :single_line +.content-block.second-block + %h2.snippet-title.prepend-top-0.append-bottom-0 + = markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml new file mode 100644 index 00000000000..d1e861ca80c --- /dev/null +++ b/app/views/shared/web_hooks/_form.html.haml @@ -0,0 +1,91 @@ +- page_title "Webhooks" +- context_title = @project ? 'project' : 'group' + +.row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0 + = page_title + %p + #{link_to "Webhooks", help_page_path("web_hooks", "web_hooks")} can be + used for binding events when something is happening within the project. + .col-lg-9.append-bottom-default + = form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f| + = form_errors(hook) + + .form-group + = f.label :url, "URL", class: 'label-light' + = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json' + .form-group + = f.label :token, "Secret Token", class: 'label-light' + = f.text_field :token, class: "form-control", placeholder: '' + %p.help-block + Use this token to validate received payloads + .form-group + = f.label :url, "Trigger", class: 'label-light' + %ul.list-unstyled + %li + = f.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = f.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This url will be triggered by a push to the repository + %li + = f.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = f.label :tag_push_events, class: 'list-label' do + %strong Tag push events + %p.light + This url will be triggered when a new tag is pushed to the repository + %li + = f.check_box :note_events, class: 'pull-left' + .prepend-left-20 + = f.label :note_events, class: 'list-label' do + %strong Comments + %p.light + This url will be triggered when someone adds a comment + %li + = f.check_box :issues_events, class: 'pull-left' + .prepend-left-20 + = f.label :issues_events, class: 'list-label' do + %strong Issues events + %p.light + This url will be triggered when an issue is created/updated/merged + %li + = f.check_box :merge_requests_events, class: 'pull-left' + .prepend-left-20 + = f.label :merge_requests_events, class: 'list-label' do + %strong Merge Request events + %p.light + This url will be triggered when a merge request is created/updated/merged + %li + = f.check_box :build_events, class: 'pull-left' + .prepend-left-20 + = f.label :build_events, class: 'list-label' do + %strong Build events + %p.light + This url will be triggered when the build status changes + %li + = f.check_box :wiki_page_events, class: 'pull-left' + .prepend-left-20 + = f.label :wiki_page_events, class: 'list-label' do + %strong Wiki Page events + %p.light + This url will be triggered when a wiki page is created/updated + .form-group + = f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox' + .checkbox + = f.label :enable_ssl_verification do + = f.check_box :enable_ssl_verification + %strong Enable SSL verification + = f.submit "Add Webhook", class: "btn btn-create" + %hr + %h5.prepend-top-default + Webhooks (#{hooks.count}) + - if hooks.any? + %ul.well-list + - hooks.each do |hook| + = render "project_hook", hook: hook + - else + %p.settings-message.text-center.append-bottom-0 + No webhooks found, add one in the form above. diff --git a/app/views/sherlock/queries/_backtrace.html.haml b/app/views/sherlock/queries/_backtrace.html.haml index 5c9294c0ab5..30e956e5f40 100644 --- a/app/views/sherlock/queries/_backtrace.html.haml +++ b/app/views/sherlock/queries/_backtrace.html.haml @@ -6,7 +6,11 @@ %ul.well-list - @query.application_backtrace.each do |location| %li - = location.path + %strong + - if defined?(BetterErrors) + = link_to(location.path, BetterErrors.editor[location.path, location.line]) + - else + = location.path %small.light = t('sherlock.line') = location.line diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml index 549b47430e6..7073c0f4d90 100644 --- a/app/views/sherlock/queries/_general.html.haml +++ b/app/views/sherlock/queries/_general.html.haml @@ -11,13 +11,17 @@ = @query.duration.round(4) = t('sherlock.milliseconds') %li + - frame = @query.last_application_frame %span.light #{t('sherlock.origin')}: %strong - = @query.last_application_frame.path + - if defined?(BetterErrors) + = link_to(frame.path, BetterErrors.editor[frame.path, frame.line]) + - else + = frame.path %small.light = t('sherlock.line') - = @query.last_application_frame.line + = frame.line .panel.panel-default .panel-heading diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 1979ae6d5bc..a7769654b61 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -1,11 +1,27 @@ -= link_to new_snippet_path, class: 'btn btn-grouped new-snippet-link', title: "New Snippet" do - = icon('plus') - New Snippet -- if can?(current_user, :update_personal_snippet, @snippet) - = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do - = icon('pencil-square-o') - Edit -- if can?(current_user, :admin_personal_snippet, @snippet) - = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-remove", title: 'Delete Snippet' do - = icon('trash-o') - Delete +.hidden-xs + = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New Snippet" do + = icon('plus') + New Snippet + - if can?(current_user, :update_personal_snippet, @snippet) + = link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do + Edit + - if can?(current_user, :admin_personal_snippet, @snippet) + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do + Delete +.visible-xs-block.dropdown + %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } + Options + %span.caret + .dropdown-menu.dropdown-menu-full-width + %ul + %li + = link_to new_snippet_path, title: "New Snippet" do + New Snippet + - if can?(current_user, :update_personal_snippet, @snippet) + %li + = link_to edit_snippet_path(@snippet) do + Edit + - if can?(current_user, :admin_personal_snippet, @snippet) + %li + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do + Delete diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index a2b36568770..ed3992650d4 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -3,11 +3,10 @@ .snippet-holder = render 'shared/snippets/header' - %article.file-holder - .file-title + %article.file-holder.file-holder-no-border.snippet-file-content + .file-title.file-title-clear = blob_icon 0, @snippet.file_name - %strong - = @snippet.file_name + = @snippet.file_name .file-actions.hidden-xs = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml new file mode 100644 index 00000000000..75fb0e303ad --- /dev/null +++ b/app/views/u2f/_authenticate.html.haml @@ -0,0 +1,28 @@ +#js-authenticate-u2f + +%script#js-authenticate-u2f-not-supported{ type: "text/template" } + %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer). + +%script#js-authenticate-u2f-setup{ type: "text/template" } + %div + %p Insert your security key (if you haven't already), and press the button below. + %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Login Via U2F Device + +%script#js-authenticate-u2f-in-progress{ type: "text/template" } + %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now. + +%script#js-authenticate-u2f-error{ type: "text/template" } + %div + %p <%= error_message %> + %a.btn.btn-warning#js-u2f-try-again Try again? + +%script#js-authenticate-u2f-authenticated{ type: "text/template" } + %div + %p We heard back from your U2F device. Click this button to authenticate with the GitLab server. + = form_tag(new_user_session_path, method: :post) do |f| + = hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response" + = submit_tag "Authenticate via U2F Device", class: "btn btn-success" + +:javascript + var u2fAuthenticate = new U2FAuthenticate($("#js-authenticate-u2f"), gon.u2f); + u2fAuthenticate.start(); diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml new file mode 100644 index 00000000000..46af591fc43 --- /dev/null +++ b/app/views/u2f/_register.html.haml @@ -0,0 +1,31 @@ +#js-register-u2f + +%script#js-register-u2f-not-supported{ type: "text/template" } + %p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer). + +%script#js-register-u2f-setup{ type: "text/template" } + .row.append-bottom-10 + .col-md-3 + %a#js-setup-u2f-device.btn.btn-info{ href: 'javascript:void(0)' } Setup New U2F Device + .col-md-9 + %p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left. + +%script#js-register-u2f-in-progress{ type: "text/template" } + %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now. + +%script#js-register-u2f-error{ type: "text/template" } + %div + %p + %span <%= error_message %> + %a.btn.btn-warning#js-u2f-try-again Try again? + +%script#js-register-u2f-registered{ type: "text/template" } + %div.row.append-bottom-10 + %p Your device was successfully set up! Click this button to register with the GitLab server. + = form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do + = hidden_field_tag :device_response, nil, class: 'form-control', required: true, id: "js-device-response" + = submit_tag "Register U2F Device", class: "btn btn-success" + +:javascript + var u2fRegister = new U2FRegister($("#js-register-u2f"), gon.u2f); + u2fRegister.start(); diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 1de71f37d1a..77f2ddefb1e 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -1,10 +1,9 @@ -#cal-heatmap.calendar - :javascript - new Calendar( - #{@timestamps.to_json}, - #{@starting_year}, - #{@starting_month}, - '#{user_calendar_activities_path}' - ); - -.calendar-hint Summary of issues, merge requests, and push events +.clearfix.calendar + .js-contrib-calendar + .calendar-hint + Summary of issues, merge requests, and push events +:javascript + new Calendar( + #{@timestamps.to_json}, + '#{user_calendar_activities_path}' + ); diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index 027a93a75fc..630d97e339d 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -1,23 +1,27 @@ %h4.prepend-top-20 - %span.light Contributions for + Contributions for %strong #{@calendar_date.to_s(:short)} -%ul.bordered-list - - @events.sort_by(&:created_at).each do |event| - %li - %span.light - %i.fa.fa-clock-o - = event.created_at.to_s(:time) - - if event.push? - #{event.action_name} #{event.ref_type} #{event.ref_name} - - else - = event_action_name(event) - - if event.target - %strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target] - - at - %strong - - if event.project - = link_to_project event.project +- if @events.any? + %ul.bordered-list + - @events.sort_by(&:created_at).each do |event| + %li + %span.light + %i.fa.fa-clock-o + = event.created_at.to_s(:time) + - if event.push? + #{event.action_name} #{event.ref_type} #{event.ref_name} - else - = event.project_name + = event_action_name(event) + - if event.target + %strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target] + + at + %strong + - if event.project + = link_to_project event.project + - else + = event.project_name +- else + %p + No contributions found for #{@calendar_date.to_s(:short)} diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder index e9e466c6350..6c85e5f9fbd 100644 --- a/app/views/users/show.atom.builder +++ b/app/views/users/show.atom.builder @@ -6,7 +6,5 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.id user_url(@user) xml.updated @events[0].updated_at.xmlschema if @events[0] - @events.each do |event| - event_to_atom(xml, event) - end + xml << render(@events) if @events.any? end diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 3c0b89c6741..92305594a81 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,5 +1,6 @@ - page_title @user.name - page_description @user.bio +- page_specific_javascripts asset_path("users/application.js") - header_title @user.name, user_path(@user) - @no_container = true @@ -78,18 +79,20 @@ %li.js-contributed-tab = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do Contributed projects - %li.projects-tab + %li.js-projects-tab = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do Personal projects + %li.js-snippets-tab + = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do + Snippets %div{ class: container_class } .tab-content #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs - %div{ class: container_class } - .user-calendar{data: {href: user_calendar_path}} - %h4.center.light - %i.fa.fa-spinner.fa-spin + .user-calendar{data: {href: user_calendar_path}} + %h4.center.light + %i.fa.fa-spinner.fa-spin .user-calendar-activities .content_list{ data: {href: user_path} } @@ -104,6 +107,9 @@ #projects.tab-pane - # This tab is always loaded via AJAX + #snippets.tab-pane + - # This tab is always loaded via AJAX + .loading-status = spinner diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml deleted file mode 100644 index 4beb8746444..00000000000 --- a/app/views/votes/_votes_block.html.haml +++ /dev/null @@ -1,30 +0,0 @@ -.awards.votes-block - - awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes| - %button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), data: {placement: "top", original_title: emoji_author_list(notes, current_user)}} - = emoji_icon(emoji, sprite: false) - %span.award-control-text.js-counter - = notes.count - - - if current_user - %div.award-menu-holder.js-award-holder - %a.btn.award-control.js-add-award{"href" => "#"} - = icon('smile-o', {class: "award-control-icon"}) - = icon('spinner spin', {class: "award-control-icon award-control-icon-loading"}) - %span.award-control-text - Add - -- if current_user - :javascript - var getEmojisUrl = "#{emojis_path}"; - var postEmojiUrl = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}"; - var noteableType = "#{votable.class.name.underscore}"; - var noteableId = "#{votable.id}"; - var unicodes = #{AwardEmoji.unicode.to_json}; - - window.awardsHandler = new AwardsHandler( - getEmojisUrl, - postEmojiUrl, - noteableType, - noteableId, - unicodes - ); diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index c4d8595d45d..971f969e25e 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -1,6 +1,9 @@ class EmailsOnPushWorker include Sidekiq::Worker + sidekiq_options queue: :mailers + attr_reader :email, :skip_premailer + def perform(project_id, recipients, push_data, options = {}) options.symbolize_keys! options.reverse_merge!( @@ -25,15 +28,18 @@ class EmailsOnPushWorker :push end + diff_refs = nil compare = nil reverse_compare = false if action == :push compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) + diff_refs = [project.merge_base_commit(before_sha, after_sha), project.commit(after_sha)] return false if compare.same if compare.commits.empty? compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha) + diff_refs = [project.merge_base_commit(after_sha, before_sha), project.commit(before_sha)] reverse_compare = true @@ -41,26 +47,42 @@ class EmailsOnPushWorker end end - recipients.split(" ").each do |recipient| + recipients.split.each do |recipient| begin - Notify.repository_push_email( - project_id, + send_email( recipient, - author_id: author_id, - ref: ref, - action: action, - compare: compare, - reverse_compare: reverse_compare, - send_from_committer_email: send_from_committer_email, - disable_diffs: disable_diffs - ).deliver_now + project_id, + author_id: author_id, + ref: ref, + action: action, + compare: compare, + reverse_compare: reverse_compare, + diff_refs: diff_refs, + send_from_committer_email: send_from_committer_email, + disable_diffs: disable_diffs + ) + # These are input errors and won't be corrected even if Sidekiq retries rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e logger.info("Failed to send e-mail for project '#{project.name_with_namespace}' to #{recipient}: #{e}") end end ensure + @email = nil compare = nil GC.start end + + private + + def send_email(recipient, project_id, options) + # Generating the body of this email can be expensive, so only do it once + @skip_premailer ||= email.present? + @email ||= Notify.repository_push_email(project_id, options) + + email.to = recipient + email.add_message_id + email.header[:skip_premailer] = true if skip_premailer + email.deliver_now + end end diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index f9e32337983..d947f105516 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -15,8 +15,7 @@ class RepositoryForkWorker result = gitlab_shell.fork_repository(source_path, target_path) unless result logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}") - project.update(import_error: "The project could not be forked.") - project.import_fail + project.mark_import_as_failed('The project could not be forked.') return end @@ -24,8 +23,7 @@ class RepositoryForkWorker unless project.valid_repo? logger.error("Project #{project_id} had an invalid repository after fork") - project.update(import_error: "The forked repository is invalid.") - project.import_fail + project.mark_import_as_failed('The forked repository is invalid.') return end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 2937493c614..7d819fe78f8 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -13,8 +13,7 @@ class RepositoryImportWorker result = Projects::ImportService.new(project, current_user).execute if result[:status] == :error - project.update(import_error: result[:message]) - project.import_fail + project.mark_import_as_failed(result[:message]) return end diff --git a/config/application.rb b/config/application.rb index b602e2b6168..49d4d3ba555 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,23 +1,32 @@ require File.expand_path('../boot', __FILE__) require 'rails/all' -require 'devise' -I18n.config.enforce_available_locales = false + Bundler.require(:default, Rails.env) -require_relative '../lib/gitlab/redis' module Gitlab class Application < Rails::Application + require_dependency Rails.root.join('lib/gitlab/redis') + # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. - # Custom directories with classes and modules you want to be autoloadable. - config.autoload_paths.push(*%W(#{config.root}/lib - #{config.root}/app/models/hooks - #{config.root}/app/models/concerns - #{config.root}/app/models/project_services - #{config.root}/app/models/members)) + # Sidekiq uses eager loading, but directories not in the standard Rails + # directories must be added to the eager load paths: + # https://github.com/mperham/sidekiq/wiki/FAQ#why-doesnt-sidekiq-autoload-my-rails-application-code + # Also, there is no need to add `lib` to autoload_paths since autoloading is + # configured to check for eager loaded paths: + # https://github.com/rails/rails/blob/v4.2.6/railties/lib/rails/engine.rb#L687 + # This is a nice reference article on autoloading/eager loading: + # http://blog.arkency.com/2014/11/dont-forget-about-eager-load-when-extending-autoload + config.eager_load_paths.push(*%W(#{config.root}/lib + #{config.root}/app/models/ci + #{config.root}/app/models/hooks + #{config.root}/app/models/members + #{config.root}/app/models/project_services)) + + config.generators.templates.push("#{config.root}/generator_templates") # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named. @@ -32,7 +41,7 @@ module Gitlab config.encoding = "utf-8" # Configure sensitive parameters which will be filtered from the log file. - # + # # Parameters filtered: # - Password (:password, :password_confirmation) # - Private tokens (:private_token) @@ -71,6 +80,9 @@ module Gitlab config.assets.precompile << "*.png" config.assets.precompile << "print.css" config.assets.precompile << "notify.css" + config.assets.precompile << "mailers/*.css" + config.assets.precompile << "graphs/application.js" + config.assets.precompile << "users/application.js" # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' diff --git a/config/boot.rb b/config/boot.rb index 4489e58688c..f2830ae3166 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -3,4 +3,4 @@ require 'rubygems' # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml new file mode 100644 index 00000000000..436a2c5e17a --- /dev/null +++ b/config/dependency_decisions.yml @@ -0,0 +1,183 @@ +--- +# IGNORED GROUPS AND GEMS +- - :ignore_group + - development + - :who: Connor Shea + :why: Development gems are not distributed with the final product and are therefore exempt. + :versions: [] + :when: 2016-04-17 21:27:01.054140000 Z +- - :ignore_group + - test + - :who: Connor Shea + :why: Test gems are not distributed with the final product and are therefore exempt. + :versions: [] + :when: 2016-04-17 21:27:06.250326000 Z +- - :ignore + - bundler + - :who: Connor Shea + :why: Bundler is MIT licensed but will sometimes fail in CI. + :versions: [] + :when: 2016-05-02 06:42:08.045090000 Z + +# LICENSE WHITELIST +- - :whitelist + - MIT + - :who: Connor Shea + :why: http://choosealicense.com/licenses/mit/ + :versions: [] + :when: 2016-04-17 21:12:24.558441000 Z +- - :whitelist + - Apache 2.0 + - :who: Connor Shea + :why: http://choosealicense.com/licenses/apache-2.0/ + :versions: [] + :when: 2016-05-02 05:27:43.762702000 Z +- - :whitelist + - ruby + - :who: Connor Shea + :why: https://github.com/ruby/ruby/blob/ruby_2_1/COPYING + :versions: [] + :when: 2016-05-02 05:31:54.498490000 Z +- - :whitelist + - LGPL + - :who: Connor Shea + :why: http://www.gnu.org/licenses/license-list.html#LGPLv2.1 + :versions: [] + :when: 2016-05-02 05:32:48.645841000 Z +- - :whitelist + - ISC + - :who: Connor Shea + :why: http://www.gnu.org/licenses/license-list.html#ISC + :versions: [] + :when: 2016-05-02 05:42:01.894452000 Z +- - :whitelist + - New BSD + - :who: Connor Shea + :why: https://opensource.org/licenses/BSD-3-Clause + :versions: [] + :when: 2016-05-02 05:44:38.246021000 Z +- - :whitelist + - LGPL-2.1+ + - :who: Connor Shea + :why: Equivalent to LGPL. + :versions: [] + :when: 2016-05-02 05:52:56.303239000 Z +- - :whitelist + - BSD + - :who: Connor Shea + :why: https://opensource.org/licenses/BSD-2-Clause + :versions: [] + :when: 2016-05-02 05:55:09.796363000 Z + +# LICENSE BLACKLIST +- - :blacklist + - GPLv2 + - :who: Connor Shea + :why: GPL-licensed libraries cannot be linked to from non-GPL projects. + :versions: [] + :when: 2016-05-02 05:29:27.637336000 Z +- - :blacklist + - GPLv3 + - :who: Connor Shea + :why: GPL-licensed libraries cannot be linked to from non-GPL projects. + :versions: [] + :when: 2016-05-02 05:29:43.904715000 Z + +# GEM LICENSES +- - :license + - raphael-rails + - MIT + - :who: Connor Shea + :why: https://github.com/mockdeep/raphael-rails/blob/master/license.txt + :versions: [] + :when: 2016-04-17 21:30:07.575392000 Z +- - :license + - rouge + - MIT + - :who: Connor Shea + :why: https://github.com/jneen/rouge/blob/master/LICENSE + :versions: [] + :when: 2016-04-17 21:31:29.490394000 Z +- - :license + - pyu-ruby-sasl + - MIT + - :who: Connor Shea + :why: https://github.com/pyu10055/ruby-sasl/blob/master/MIT-LICENSE + :versions: [] + :when: 2016-04-17 21:41:55.266420000 Z +- - :license + - six + - MIT + - :who: Connor Shea + :why: https://github.com/randx/six/blob/master/LICENSE + :versions: [] + :when: 2016-04-17 21:42:31.420186000 Z +- - :license + - rdoc + - ruby + - :who: Connor Shea + :why: https://github.com/rdoc/rdoc/blob/master/LICENSE.rdoc + :versions: [] + :when: 2016-04-17 21:43:30.480413000 Z +- - :license + - expression_parser + - MIT + - :who: Connor Shea + :why: https://github.com/nricciar/expression_parser/blob/master/MIT-LICENSE + :versions: [] + :when: 2016-04-17 21:45:41.829912000 Z +- - :license + - creole + - ruby + - :who: Connor Shea + :why: https://github.com/minad/creole#license + :versions: [] + :when: 2016-04-17 21:49:10.329759000 Z +- - :license + - eventmachine + - ruby + - :who: Connor Shea + :why: https://github.com/eventmachine/eventmachine/blob/master/LICENSE + :versions: [] + :when: 2016-04-17 21:49:10.329759001 Z +- - :license + - unicorn + - ruby + - :who: Connor Shea + :why: http://unicorn.bogomips.org/LICENSE.html + :versions: [] + :when: 2016-05-02 05:45:28.817510000 Z +- - :license + - unicorn-worker-killer + - ruby + - :who: Connor Shea + :why: https://github.com/kzk/unicorn-worker-killer/blob/master/LICENSE + :versions: [] + :when: 2016-05-02 05:45:38.323867000 Z +- - :license + - json + - ruby + - :who: Connor Shea + :why: https://github.com/flori/json/tree/master#license + :versions: [] + :when: 2016-05-02 05:50:07.826564000 Z +- - :license + - unf + - BSD + - :who: Connor Shea + :why: https://github.com/knu/ruby-unf/blob/master/LICENSE + :versions: [] + :when: 2016-05-02 05:51:46.886872000 Z +- - :license + - rubypants + - BSD + - :who: Connor Shea + :why: https://github.com/jmcnevin/rubypants/blob/master/LICENSE.rdoc + :versions: [] + :when: 2016-05-02 05:56:50.696858000 Z +- - :whitelist + - LGPLv2+ + - :who: Stan Hu + :why: Equivalent to LGPLv2 + :versions: [] + :when: 2016-06-07 17:14:10.907682000 Z diff --git a/config/environments/development.rb b/config/environments/development.rb index 4f39016bfa4..8cca0039b4a 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -39,6 +39,7 @@ Rails.application.configure do config.action_mailer.delivery_method = :letter_opener_web # Don't make a mess when bootstrapping a development environment config.action_mailer.perform_deliveries = (ENV['BOOTSTRAP'] != '1') + config.action_mailer.preview_path = 'spec/mailers/previews' config.eager_load = false end diff --git a/config/environments/test.rb b/config/environments/test.rb index a703c0934f7..fb25d3a8b14 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -20,7 +20,7 @@ Rails.application.configure do config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment - config.action_controller.allow_forgery_protection = false + config.action_controller.allow_forgery_protection = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index e682bcb976d..1048ef6e243 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -98,6 +98,7 @@ production: &base wiki: true snippets: false builds: true + container_registry: true ## Webhook settings # Number of seconds to wait for HTTP response after sending webhook HTTP POST request (default: 10) @@ -175,6 +176,15 @@ production: &base repository_archive_cache_worker: cron: "0 * * * *" + registry: + # enabled: true + # host: registry.example.com + # port: 5005 + # api_url: http://localhost:5000/ # internal address to the registry, will be used by GitLab to directly communicate with API + # key: config/registry.key + # path: shared/registry + # issuer: gitlab-issuer + # # 2. GitLab CI settings # ========================== diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 8db2c05fe45..436751b9d16 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -1,4 +1,4 @@ -require 'gitlab' # Load lib/gitlab.rb as soon as possible +require_dependency Rails.root.join('lib/gitlab') # Load Gitlab as soon as possible class Settings < Settingslogic source ENV.fetch('GITLAB_CONFIG') { "#{Rails.root}/config/gitlab.yml" } @@ -52,7 +52,7 @@ class Settings < Settingslogic # check that values in `current` (string or integer) is a contant in `modul`. def verify_constant_array(modul, current, default) values = default || [] - if !current.nil? + unless current.nil? values = [] current.each do |constant| values.push(verify_constant(modul, constant, nil)) @@ -126,7 +126,7 @@ end Settings['omniauth'] ||= Settingslogic.new({}) -Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? +Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil? Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['auto_sign_in_with_provider'].nil? Settings.omniauth['allow_single_sign_on'] = false if Settings.omniauth['allow_single_sign_on'].nil? Settings.omniauth['external_providers'] = [] if Settings.omniauth['external_providers'].nil? @@ -134,7 +134,7 @@ Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil? Settings.omniauth['auto_link_saml_user'] = false if Settings.omniauth['auto_link_saml_user'].nil? -Settings.omniauth['providers'] ||= [] +Settings.omniauth['providers'] ||= [] Settings.omniauth['cas3'] ||= Settingslogic.new({}) Settings.omniauth.cas3['session_duration'] ||= 8.hours Settings.omniauth['session_tickets'] ||= Settingslogic.new({}) @@ -168,7 +168,7 @@ end Settings['shared'] ||= Settingslogic.new({}) Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root) -Settings['issues_tracker'] ||= {} +Settings['issues_tracker'] ||= {} # # GitLab @@ -183,7 +183,7 @@ Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? Settings.gitlab['port'] ||= Settings.gitlab.https ? 443 : 80 Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || '' -Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" +Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil? Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}" Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab' @@ -196,7 +196,7 @@ Settings.gitlab['user_home'] ||= begin rescue ArgumentError # no user configured '/home/' + Settings.gitlab['user'] end -Settings.gitlab['time_zone'] ||= nil +Settings.gitlab['time_zone'] ||= nil Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].nil? Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) @@ -206,12 +206,13 @@ Settings.gitlab['default_projects_features'] ||= {} Settings.gitlab['webhook_timeout'] ||= 10 Settings.gitlab['max_attachment_size'] ||= 10 Settings.gitlab['session_expire_delay'] ||= 10080 -Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? -Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil? -Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? -Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? -Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? -Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) +Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil? +Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil? +Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil? +Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil? +Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil? +Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil? +Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil? Settings.gitlab['restricted_signup_domains'] ||= [] Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'] @@ -225,8 +226,8 @@ Settings['gitlab_ci'] ||= Settingslogic.new({}) Settings.gitlab_ci['shared_runners_enabled'] = true if Settings.gitlab_ci['shared_runners_enabled'].nil? Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_broken_builds'].nil? Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil? -Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url) Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci['builds_path'] || "builds/", Rails.root) +Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url) # # Reply by email @@ -240,7 +241,20 @@ Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'] Settings['artifacts'] ||= Settingslogic.new({}) Settings.artifacts['enabled'] = true if Settings.artifacts['enabled'].nil? Settings.artifacts['path'] = File.expand_path(Settings.artifacts['path'] || File.join(Settings.shared['path'], "artifacts"), Rails.root) -Settings.artifacts['max_size'] ||= 100 # in megabytes +Settings.artifacts['max_size'] ||= 100 # in megabytes + +# +# Registry +# +Settings['registry'] ||= Settingslogic.new({}) +Settings.registry['enabled'] ||= false +Settings.registry['host'] ||= "example.com" +Settings.registry['port'] ||= nil +Settings.registry['api_url'] ||= "http://localhost:5000/" +Settings.registry['key'] ||= nil +Settings.registry['issuer'] ||= nil +Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.registry['port']].compact.join(':') +Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'), Rails.root) # # Git LFS @@ -298,7 +312,7 @@ Settings['backup'] ||= Settingslogic.new({}) Settings.backup['keep_time'] ||= 0 Settings.backup['pg_schema'] = nil Settings.backup['path'] = File.expand_path(Settings.backup['path'] || "tmp/backups/", Rails.root) -Settings.backup['archive_permissions'] ||= 0600 +Settings.backup['archive_permissions'] ||= 0600 Settings.backup['upload'] ||= Settingslogic.new({ 'remote_directory' => nil, 'connection' => nil }) # Convert upload connection settings to use symbol keys, to make Fog happy if Settings.backup['upload']['connection'] diff --git a/config/initializers/5_backend.rb b/config/initializers/5_backend.rb index 80d641d73a3..e026151a032 100644 --- a/config/initializers/5_backend.rb +++ b/config/initializers/5_backend.rb @@ -1,11 +1,11 @@ # GIT over HTTP -require Rails.root.join("lib", "gitlab", "backend", "grack_auth") +require_dependency Rails.root.join('lib/gitlab/backend/grack_auth') # GIT over SSH -require Rails.root.join("lib", "gitlab", "backend", "shell") +require_dependency Rails.root.join('lib/gitlab/backend/shell') # GitLab shell adapter -require Rails.root.join("lib", "gitlab", "backend", "shell_adapter") +require_dependency Rails.root.join('lib/gitlab/backend/shell_adapter') required_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version_required) current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.new.version) diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index df28d30d750..1933afcbfb1 100644 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -2,7 +2,7 @@ CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/ aws_file = Rails.root.join('config', 'aws.yml') -if File.exists?(aws_file) +if File.exist?(aws_file) AWS_CONFIG = YAML.load(File.read(aws_file))[Rails.env] CarrierWave.configure do |config| @@ -20,7 +20,7 @@ if File.exists?(aws_file) config.fog_public = false # optional, defaults to {} - config.fog_attributes = { 'Cache-Control'=>'max-age=315576000' } + config.fog_attributes = { 'Cache-Control' => 'max-age=315576000' } # optional time (in seconds) that authenticated urls will be valid. # when fog_public is false and provider is AWS or Google, defaults to 600 diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 31dceaebcad..021bdb11251 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -243,7 +243,7 @@ Devise.setup do |config| when Hash # Add procs for handling SLO if provider['name'] == 'cas3' - provider['args'][:on_single_sign_out] = lambda do |request| + provider['args'][:on_single_sign_out] = lambda do |request| ticket = request.params[:session_index] raise "Service Ticket not found." unless Gitlab::OAuth::Session.valid?(:cas3, ticket) Gitlab::OAuth::Session.destroy(:cas3, ticket) diff --git a/config/initializers/devise_async.rb b/config/initializers/devise_async.rb deleted file mode 100644 index 05a1852cdbd..00000000000 --- a/config/initializers/devise_async.rb +++ /dev/null @@ -1 +0,0 @@ -Devise::Async.backend = :sidekiq diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 66ac88e9f4a..8dc8e270afc 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -12,7 +12,7 @@ Doorkeeper.configure do end resource_owner_from_credentials do |routes| - Gitlab::Auth.new.find(params[:username], params[:password]) + Gitlab::Auth.find_in_gitlab_or_ldap(params[:username], params[:password]) end # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. @@ -52,7 +52,7 @@ Doorkeeper.configure do # For more information go to # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes default_scopes :api - #optional_scopes :write, :update + # optional_scopes :write, :update # Change the way client credentials are retrieved from the request object. # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then @@ -71,7 +71,7 @@ Doorkeeper.configure do # The value can be any string. Use nil to disable this feature. When disabled, clients must provide a valid URL # (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi) # - native_redirect_uri nil#'urn:ietf:wg:oauth:2.0:oob' + native_redirect_uri nil # 'urn:ietf:wg:oauth:2.0:oob' # Specify what grant flows are enabled in array of Strings. The valid # strings and the flows they enable are: diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb new file mode 100644 index 00000000000..79e2d23ab2e --- /dev/null +++ b/config/initializers/health_check.rb @@ -0,0 +1,3 @@ +HealthCheck.setup do |config| + config.standard_checks = ['database', 'migrations', 'cache'] +end diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 9e8b0131f8f..3d1a41a4652 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -8,3 +8,7 @@ # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end +# +ActiveSupport::Inflector.inflections do |inflect| + inflect.uncountable %w(award_emoji) +end diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index b2d08d87bac..2673093b96a 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -12,6 +12,7 @@ if Gitlab::Metrics.enabled? Gitlab::Application.configure do |config| config.middleware.use(Gitlab::Metrics::RackMiddleware) + config.middleware.use(Gitlab::Middleware::RailsQueueDuration) end Sidekiq.configure_server do |config| @@ -118,6 +119,15 @@ if Gitlab::Metrics.enabled? # Instrument the classes used for checking if somebody has push access. config.instrument_instance_methods(Gitlab::GitAccess) config.instrument_instance_methods(Gitlab::GitAccessWiki) + + config.instrument_instance_methods(API::Helpers) + + config.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker) + # Iterate over each non-super private instance method to keep up to date if + # internals change + RepositoryCheck::SingleRepositoryWorker.private_instance_methods(false).each do |method| + config.instrument_instance_method(RepositoryCheck::SingleRepositoryWorker, method) + end end GC::Profiler.enable diff --git a/config/initializers/monkey_patch.rb b/config/initializers/monkey_patch.rb deleted file mode 100644 index 62b05a55285..00000000000 --- a/config/initializers/monkey_patch.rb +++ /dev/null @@ -1,48 +0,0 @@ -## This patch is from rails 4.2-stable. Remove it when 4.2.6 is released -## https://github.com/rails/rails/issues/21108 - -module ActiveRecord - module ConnectionAdapters - class AbstractMysqlAdapter < AbstractAdapter - # SHOW VARIABLES LIKE 'name' - def show_variable(name) - variables = select_all("select @@#{name} as 'Value'", 'SCHEMA') - variables.first['Value'] unless variables.empty? - rescue ActiveRecord::StatementInvalid - nil - end - - - # MySQL is too stupid to create a temporary table for use subquery, so we have - # to give it some prompting in the form of a subsubquery. Ugh! - def subquery_for(key, select) - subsubselect = select.clone - subsubselect.projections = [key] - - subselect = Arel::SelectManager.new(select.engine) - subselect.project Arel.sql(key.name) - # Materialized subquery by adding distinct - # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' - subselect.from subsubselect.distinct.as('__active_record_temp') - end - end - end -end - -module ActiveRecord - module ConnectionAdapters - class MysqlAdapter < AbstractMysqlAdapter - ADAPTER_NAME = 'MySQL'.freeze - - # Get the client encoding for this database - def client_encoding - return @client_encoding if @client_encoding - - result = exec_query( - "select @@character_set_client", - 'SCHEMA') - @client_encoding = ENCODINGS[result.rows.last.last] - end - end - end -end diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 4c164119fff..26c30e523a7 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -13,7 +13,7 @@ end OmniAuth.config.full_host = Settings.gitlab['base_url'] OmniAuth.config.allowed_request_methods = [:post] -#In case of auto sign-in, the GET method is used (users don't get to click on a button) +# In case of auto sign-in, the GET method is used (users don't get to click on a button) OmniAuth.config.allowed_request_methods << :get if Gitlab.config.omniauth.auto_sign_in_with_provider.present? OmniAuth.config.before_request_phase do |env| OmniAuth::RequestForgeryProtection.call(env) diff --git a/config/initializers/premailer.rb b/config/initializers/premailer.rb index b9176688bc4..cb00d3cfe95 100644 --- a/config/initializers/premailer.rb +++ b/config/initializers/premailer.rb @@ -3,6 +3,6 @@ Premailer::Rails.config.merge!( generate_text_part: false, preserve_styles: true, remove_comments: true, - remove_ids: true, + remove_ids: false, remove_scripts: false ) diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 599dabb9e50..0d9d87bac00 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -23,6 +23,6 @@ else secure: Gitlab.config.gitlab.https, httponly: true, expires_in: Settings.gitlab['session_expire_delay'] * 60, - path: (Rails.application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root + path: Rails.application.config.relative_url_root.nil? ? '/' : Gitlab::Application.config.relative_url_root ) end diff --git a/config/license_finder.yml b/config/license_finder.yml new file mode 100644 index 00000000000..e01ebec3298 --- /dev/null +++ b/config/license_finder.yml @@ -0,0 +1,2 @@ +--- +decisions_file: './config/dependency_decisions.yml' diff --git a/config/mail_room.yml b/config/mail_room.yml index 761a32adb9e..7cab24b295e 100644 --- a/config/mail_room.yml +++ b/config/mail_room.yml @@ -2,7 +2,7 @@ <% require "yaml" require "json" -require_relative "lib/gitlab/redis" +require_relative "lib/gitlab/redis" unless defined?(Gitlab::Redis) rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" diff --git a/config/routes.rb b/config/routes.rb index ad44c8f05b2..e2492fbab46 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -56,6 +56,7 @@ Rails.application.routes.draw do # Autocomplete get '/autocomplete/users' => 'autocomplete#users' get '/autocomplete/users/:id' => 'autocomplete#user' + get '/autocomplete/projects' => 'autocomplete#projects' # Emojis resources :emojis, only: :index @@ -64,6 +65,9 @@ Rails.application.routes.draw do get 'search' => 'search#show' get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete + # JSON Web Token + get 'jwt/auth' => 'jwt#auth' + # API API::API.logger Rails.logger mount API::API => '/api' @@ -73,14 +77,17 @@ Rails.application.routes.draw do mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq end - # Enable Grack support - mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put] + # Health check + get 'health_check(/:checks)' => 'health_check#index', as: :health_check + + # Enable Grack support (for LFS only) + mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\/(info\/lfs|gitlab-lfs)/.match(request.path_info) }, via: [:get, :post, :put] # Help get 'help' => 'help#index' get 'help/:category/:file' => 'help#show', as: :help_page, constraints: { category: /.*/, file: /[^\/\.]+/ } get 'help/shortcuts' - get 'help/ui' => 'help#ui' + get 'help/ui' => 'help#ui' # # Global snippets @@ -91,7 +98,8 @@ Rails.application.routes.draw do end end - get '/s/:username' => 'snippets#index', as: :user_snippets, constraints: { username: /.*/ } + get '/s/:username', to: redirect('/u/%{username}/snippets'), + constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } # # Invites @@ -257,6 +265,7 @@ Rails.application.routes.draw do end resource :logs, only: [:show] + resource :health_check, controller: 'health_check', only: [:show] resource :background_jobs, controller: 'background_jobs', only: [:show] resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do @@ -288,6 +297,7 @@ Rails.application.routes.draw do resource :application_settings, only: [:show, :update] do resources :services put :reset_runners_token + put :reset_health_check_token put :clear_repository_check_states end @@ -337,8 +347,9 @@ Rails.application.routes.draw do resources :keys resources :emails, only: [:index, :create, :destroy] resource :avatar, only: [:destroy] - resource :two_factor_auth, only: [:new, :create, :destroy] do + resource :two_factor_auth, only: [:show, :create, :destroy] do member do + post :create_u2f post :codes patch :skip end @@ -346,23 +357,18 @@ Rails.application.routes.draw do end end - get 'u/:username/calendar' => 'users#calendar', as: :user_calendar, - constraints: { username: /.*/ } - - get 'u/:username/calendar_activities' => 'users#calendar_activities', as: :user_calendar_activities, - constraints: { username: /.*/ } - - get 'u/:username/groups' => 'users#groups', as: :user_groups, - constraints: { username: /.*/ } - - get 'u/:username/projects' => 'users#projects', as: :user_projects, - constraints: { username: /.*/ } - - get 'u/:username/contributed' => 'users#contributed', as: :user_contributed_projects, - constraints: { username: /.*/ } - - get '/u/:username' => 'users#show', as: :user, - constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } + scope(path: 'u/:username', + as: :user, + constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, + controller: :users) do + get :calendar + get :calendar_activities + get :groups + get :projects + get :contributed, as: :contributed_projects + get :snippets + get '/', action: :show + end # # Dashboard Area @@ -420,7 +426,11 @@ Rails.application.routes.draw do resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create] - devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, registrations: :registrations , passwords: :passwords, sessions: :sessions, confirmations: :confirmations } + devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, + registrations: :registrations, + passwords: :passwords, + sessions: :sessions, + confirmations: :confirmations } devise_scope :user do get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error @@ -435,6 +445,7 @@ Rails.application.routes.draw do resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except: [:new, :create, :index], path: "/") do + member do put :transfer delete :remove_fork @@ -448,6 +459,29 @@ Rails.application.routes.draw do end scope module: :projects do + # Git HTTP clients ('git clone' etc.) + scope constraints: { id: /.+\.git/, format: nil } do + get '/info/refs', to: 'git_http#info_refs' + post '/git-upload-pack', to: 'git_http#git_upload_pack' + post '/git-receive-pack', to: 'git_http#git_receive_pack' + end + + # Allow /info/refs, /info/refs?service=git-upload-pack, and + # /info/refs?service=git-receive-pack, but nothing else. + # + git_http_handshake = lambda do |request| + request.query_string.blank? || + request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/) + end + + ref_redirect = redirect do |params, request| + path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs" + path << "?#{request.query_string}" unless request.query_string.blank? + path + end + + get '/info/refs', constraints: git_http_handshake, to: ref_redirect + # Blob routes: get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob' post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob' @@ -586,7 +620,6 @@ Rails.application.routes.draw do # Order matters to give priority to these matches get '/wikis/git_access', to: 'wikis#git_access' get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages' - post '/wikis/markdown_preview', to:'wikis#markdown_preview' post '/wikis', to: 'wikis#create' get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID @@ -595,6 +628,7 @@ Rails.application.routes.draw do get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID + post '/wikis/*id/markdown_preview', to:'wikis#markdown_preview', constraints: WIKI_SLUG_ID, as: 'wiki_markdown_preview' end resource :repository, only: [:show, :create] do @@ -647,6 +681,7 @@ Rails.application.routes.draw do post :cancel_merge_when_build_succeeds get :ci_status post :toggle_subscription + post :toggle_award_emoji post :remove_wip end @@ -663,9 +698,16 @@ Rails.application.routes.draw do end resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } - resource :variables, only: [:show, :update] + resources :variables, only: [:index, :show, :update, :create, :destroy] resources :triggers, only: [:index, :create, :destroy] + resources :pipelines, only: [:index, :new, :create, :show] do + member do + post :cancel + post :retry + end + end + resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do collection do post :cancel_all @@ -676,6 +718,7 @@ Rails.application.routes.draw do post :cancel post :retry post :erase + get :trace get :raw end @@ -692,6 +735,8 @@ Rails.application.routes.draw do end end + resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex } + resources :milestones, constraints: { id: /\d+/ } do member do put :sort_issues @@ -702,16 +747,19 @@ Rails.application.routes.draw do resources :labels, constraints: { id: /\d+/ } do collection do post :generate + post :set_priorities end member do post :toggle_subscription + delete :remove_priority end end resources :issues, constraints: { id: /\d+/ } do member do post :toggle_subscription + post :toggle_award_emoji get :referenced_merge_requests get :related_branches get :can_create_branch @@ -740,12 +788,9 @@ Rails.application.routes.draw do resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do member do + post :toggle_award_emoji delete :delete_attachment end - - collection do - post :award_toggle - end end resources :uploads, only: [:create] do @@ -778,7 +823,7 @@ Rails.application.routes.draw do end # Get all keys of user - get ':username.keys' => 'profiles/keys#get_keys' , constraints: { username: /.*/ } + get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: /.*/ } get ':id' => 'namespaces#show', constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } end diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb index b99d24a03c9..51ff451eb4c 100644 --- a/db/fixtures/development/14_builds.rb +++ b/db/fixtures/development/14_builds.rb @@ -19,7 +19,7 @@ class Gitlab::Seeder::Builds commits = @project.repository.commits('master', nil, 5) commits_sha = commits.map { |commit| commit.raw.id } commits_sha.map do |sha| - @project.ensure_ci_commit(sha, 'master') + @project.ensure_pipeline(sha, 'master') end rescue [] diff --git a/db/fixtures/production/001_admin.rb b/db/fixtures/production/001_admin.rb index 78746c83225..b37dc794015 100644 --- a/db/fixtures/production/001_admin.rb +++ b/db/fixtures/production/001_admin.rb @@ -16,21 +16,21 @@ user = User.new(user_args) user.skip_confirmation! if user.save - puts "Administrator account created:".green + puts "Administrator account created:".color(:green) puts - puts "login: root".green + puts "login: root".color(:green) if user_args.key?(:password) - puts "password: #{user_args[:password]}".green + puts "password: #{user_args[:password]}".color(:green) else - puts "password: You'll be prompted to create one on your first visit.".green + puts "password: You'll be prompted to create one on your first visit.".color(:green) end puts else - puts "Could not create the default administrator account:".red + puts "Could not create the default administrator account:".color(:red) puts user.errors.full_messages.map do |message| - puts "--> #{message}".red + puts "--> #{message}".color(:red) end puts diff --git a/db/migrate/20121220064453_init_schema.rb b/db/migrate/20121220064453_init_schema.rb index d7644b6847a..f93dc92b70f 100644 --- a/db/migrate/20121220064453_init_schema.rb +++ b/db/migrate/20121220064453_init_schema.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class InitSchema < ActiveRecord::Migration def up diff --git a/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb b/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb index d0fca269871..84fd2060770 100644 --- a/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb +++ b/db/migrate/20130102143055_rename_owner_to_creator_for_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RenameOwnerToCreatorForProject < ActiveRecord::Migration def change rename_column :projects, :owner_id, :creator_id diff --git a/db/migrate/20130110172407_add_public_to_project.rb b/db/migrate/20130110172407_add_public_to_project.rb index 45edba48152..4362aadcc1d 100644 --- a/db/migrate/20130110172407_add_public_to_project.rb +++ b/db/migrate/20130110172407_add_public_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPublicToProject < ActiveRecord::Migration def change add_column :projects, :public, :boolean, default: false, null: false diff --git a/db/migrate/20130123114545_add_issues_tracker_to_project.rb b/db/migrate/20130123114545_add_issues_tracker_to_project.rb index 288d0f07c9a..ba8c50b53e2 100644 --- a/db/migrate/20130123114545_add_issues_tracker_to_project.rb +++ b/db/migrate/20130123114545_add_issues_tracker_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIssuesTrackerToProject < ActiveRecord::Migration def change add_column :projects, :issues_tracker, :string, default: :gitlab, null: false diff --git a/db/migrate/20130125090214_add_user_permissions.rb b/db/migrate/20130125090214_add_user_permissions.rb index 38b5f439a2d..1350eadb60e 100644 --- a/db/migrate/20130125090214_add_user_permissions.rb +++ b/db/migrate/20130125090214_add_user_permissions.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUserPermissions < ActiveRecord::Migration def up add_column :users, :can_create_group, :boolean, default: true, null: false diff --git a/db/migrate/20130131070232_remove_private_flag_from_project.rb b/db/migrate/20130131070232_remove_private_flag_from_project.rb index 5754db11558..f0273ba448e 100644 --- a/db/migrate/20130131070232_remove_private_flag_from_project.rb +++ b/db/migrate/20130131070232_remove_private_flag_from_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemovePrivateFlagFromProject < ActiveRecord::Migration def up remove_column :projects, :private_flag diff --git a/db/migrate/20130206084024_add_description_to_namsespace.rb b/db/migrate/20130206084024_add_description_to_namsespace.rb index ef02e489d03..62676ce8914 100644 --- a/db/migrate/20130206084024_add_description_to_namsespace.rb +++ b/db/migrate/20130206084024_add_description_to_namsespace.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDescriptionToNamsespace < ActiveRecord::Migration def change add_column :namespaces, :description, :string, default: '', null: false diff --git a/db/migrate/20130207104426_add_description_to_teams.rb b/db/migrate/20130207104426_add_description_to_teams.rb index 6d03777901c..bd9a4767b69 100644 --- a/db/migrate/20130207104426_add_description_to_teams.rb +++ b/db/migrate/20130207104426_add_description_to_teams.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDescriptionToTeams < ActiveRecord::Migration def change add_column :user_teams, :description, :string, default: '', null: false diff --git a/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb b/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb index 71763d18aee..56b01cbf892 100644 --- a/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb +++ b/db/migrate/20130211085435_add_issues_tracker_id_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIssuesTrackerIdToProject < ActiveRecord::Migration def change add_column :projects, :issues_tracker_id, :string diff --git a/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb b/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb index 23797fe1894..4722cc13d4b 100644 --- a/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb +++ b/db/migrate/20130214154045_rename_state_to_merge_status_in_milestone.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RenameStateToMergeStatusInMilestone < ActiveRecord::Migration def change rename_column :merge_requests, :state, :merge_status diff --git a/db/migrate/20130218140952_add_state_to_issue.rb b/db/migrate/20130218140952_add_state_to_issue.rb index 062103d0e33..3a5e978a182 100644 --- a/db/migrate/20130218140952_add_state_to_issue.rb +++ b/db/migrate/20130218140952_add_state_to_issue.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddStateToIssue < ActiveRecord::Migration def change add_column :issues, :state, :string diff --git a/db/migrate/20130218141038_add_state_to_merge_request.rb b/db/migrate/20130218141038_add_state_to_merge_request.rb index ac4108ee311..e0180c755e2 100644 --- a/db/migrate/20130218141038_add_state_to_merge_request.rb +++ b/db/migrate/20130218141038_add_state_to_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddStateToMergeRequest < ActiveRecord::Migration def change add_column :merge_requests, :state, :string diff --git a/db/migrate/20130218141117_add_state_to_milestone.rb b/db/migrate/20130218141117_add_state_to_milestone.rb index c84039106bd..5f71608692c 100644 --- a/db/migrate/20130218141117_add_state_to_milestone.rb +++ b/db/migrate/20130218141117_add_state_to_milestone.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddStateToMilestone < ActiveRecord::Migration def change add_column :milestones, :state, :string diff --git a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb index 99289166e81..94c0a6845d5 100644 --- a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb +++ b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ConvertClosedToStateInIssue < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb index bd1e016d679..64a9c761352 100644 --- a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb +++ b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ConvertClosedToStateInMergeRequest < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb index d1174bc3d98..41508c2dc95 100644 --- a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb +++ b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ConvertClosedToStateInMilestone < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20130218141444_remove_merged_from_merge_request.rb b/db/migrate/20130218141444_remove_merged_from_merge_request.rb index a7bd82f5000..afa5137061e 100644 --- a/db/migrate/20130218141444_remove_merged_from_merge_request.rb +++ b/db/migrate/20130218141444_remove_merged_from_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveMergedFromMergeRequest < ActiveRecord::Migration def up remove_column :merge_requests, :merged diff --git a/db/migrate/20130218141507_remove_closed_from_issue.rb b/db/migrate/20130218141507_remove_closed_from_issue.rb index 95cc064252b..f250288bc3b 100644 --- a/db/migrate/20130218141507_remove_closed_from_issue.rb +++ b/db/migrate/20130218141507_remove_closed_from_issue.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveClosedFromIssue < ActiveRecord::Migration def up remove_column :issues, :closed diff --git a/db/migrate/20130218141536_remove_closed_from_merge_request.rb b/db/migrate/20130218141536_remove_closed_from_merge_request.rb index 371835938b2..efa12e32636 100644 --- a/db/migrate/20130218141536_remove_closed_from_merge_request.rb +++ b/db/migrate/20130218141536_remove_closed_from_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveClosedFromMergeRequest < ActiveRecord::Migration def up remove_column :merge_requests, :closed diff --git a/db/migrate/20130218141554_remove_closed_from_milestone.rb b/db/migrate/20130218141554_remove_closed_from_milestone.rb index e8dae4a19b1..75ac14e43be 100644 --- a/db/migrate/20130218141554_remove_closed_from_milestone.rb +++ b/db/migrate/20130218141554_remove_closed_from_milestone.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveClosedFromMilestone < ActiveRecord::Migration def up remove_column :milestones, :closed diff --git a/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb b/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb index d78bd0ae923..97615e47c89 100644 --- a/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb +++ b/db/migrate/20130220124204_add_new_merge_status_to_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNewMergeStatusToMergeRequest < ActiveRecord::Migration def change add_column :merge_requests, :new_merge_status, :string diff --git a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb b/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb index 1c758c56ffe..3b8c3686c55 100644 --- a/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb +++ b/db/migrate/20130220125544_convert_merge_status_in_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ConvertMergeStatusInMergeRequest < ActiveRecord::Migration def up execute "UPDATE #{table_name} SET new_merge_status = 'unchecked' WHERE merge_status = 1" diff --git a/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb b/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb index 9083183beb0..bd25ffbfc99 100644 --- a/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb +++ b/db/migrate/20130220125545_remove_merge_status_from_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveMergeStatusFromMergeRequest < ActiveRecord::Migration def up remove_column :merge_requests, :merge_status diff --git a/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb b/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb index 3f8f38dc979..f0595720a39 100644 --- a/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb +++ b/db/migrate/20130220133245_rename_new_merge_status_to_merge_status_in_milestone.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RenameNewMergeStatusToMergeStatusInMilestone < ActiveRecord::Migration def change rename_column :merge_requests, :new_merge_status, :merge_status diff --git a/db/migrate/20130304104623_add_state_to_user.rb b/db/migrate/20130304104623_add_state_to_user.rb index 8154c21065f..4456d022e3f 100644 --- a/db/migrate/20130304104623_add_state_to_user.rb +++ b/db/migrate/20130304104623_add_state_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddStateToUser < ActiveRecord::Migration def change add_column :users, :state, :string diff --git a/db/migrate/20130304104740_convert_blocked_to_state.rb b/db/migrate/20130304104740_convert_blocked_to_state.rb index e8d5257ac96..9afd1093645 100644 --- a/db/migrate/20130304104740_convert_blocked_to_state.rb +++ b/db/migrate/20130304104740_convert_blocked_to_state.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ConvertBlockedToState < ActiveRecord::Migration def up User.transaction do diff --git a/db/migrate/20130304105317_remove_blocked_from_user.rb b/db/migrate/20130304105317_remove_blocked_from_user.rb index e010474538c..8f5b2c59b43 100644 --- a/db/migrate/20130304105317_remove_blocked_from_user.rb +++ b/db/migrate/20130304105317_remove_blocked_from_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveBlockedFromUser < ActiveRecord::Migration def up remove_column :users, :blocked diff --git a/db/migrate/20130315124931_user_color_scheme.rb b/db/migrate/20130315124931_user_color_scheme.rb index 56c9a31ee3c..06e28a49d9d 100644 --- a/db/migrate/20130315124931_user_color_scheme.rb +++ b/db/migrate/20130315124931_user_color_scheme.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class UserColorScheme < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20130318212250_add_snippets_to_features.rb b/db/migrate/20130318212250_add_snippets_to_features.rb index ad0b4434c43..9860b85f504 100644 --- a/db/migrate/20130318212250_add_snippets_to_features.rb +++ b/db/migrate/20130318212250_add_snippets_to_features.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddSnippetsToFeatures < ActiveRecord::Migration def change add_column :projects, :snippets_enabled, :boolean, null: false, default: true diff --git a/db/migrate/20130319214458_create_forked_project_links.rb b/db/migrate/20130319214458_create_forked_project_links.rb index f91afc26e77..66eb11a4b2b 100644 --- a/db/migrate/20130319214458_create_forked_project_links.rb +++ b/db/migrate/20130319214458_create_forked_project_links.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateForkedProjectLinks < ActiveRecord::Migration def change create_table :forked_project_links do |t| diff --git a/db/migrate/20130323174317_add_private_to_snippets.rb b/db/migrate/20130323174317_add_private_to_snippets.rb index 92f3a5c7011..376f4618d41 100644 --- a/db/migrate/20130323174317_add_private_to_snippets.rb +++ b/db/migrate/20130323174317_add_private_to_snippets.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPrivateToSnippets < ActiveRecord::Migration def change add_column :snippets, :private, :boolean, null: false, default: true diff --git a/db/migrate/20130324151736_add_type_to_snippets.rb b/db/migrate/20130324151736_add_type_to_snippets.rb index 276aab2ca15..097cb9bc7cb 100644 --- a/db/migrate/20130324151736_add_type_to_snippets.rb +++ b/db/migrate/20130324151736_add_type_to_snippets.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTypeToSnippets < ActiveRecord::Migration def change add_column :snippets, :type, :string diff --git a/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb b/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb index 4c992bac4d1..9256e62086e 100644 --- a/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb +++ b/db/migrate/20130324172327_change_project_id_to_null_in_snipepts.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ChangeProjectIdToNullInSnipepts < ActiveRecord::Migration def up change_column :snippets, :project_id, :integer, :null => true diff --git a/db/migrate/20130324203535_add_type_value_for_snippets.rb b/db/migrate/20130324203535_add_type_value_for_snippets.rb index 8c05dd2cc71..6e910fd74c7 100644 --- a/db/migrate/20130324203535_add_type_value_for_snippets.rb +++ b/db/migrate/20130324203535_add_type_value_for_snippets.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTypeValueForSnippets < ActiveRecord::Migration def up Snippet.where("project_id IS NOT NULL").update_all(type: 'ProjectSnippet') diff --git a/db/migrate/20130325173941_add_notification_level_to_user.rb b/db/migrate/20130325173941_add_notification_level_to_user.rb index 9f466e38c13..1dc58d4bcc8 100644 --- a/db/migrate/20130325173941_add_notification_level_to_user.rb +++ b/db/migrate/20130325173941_add_notification_level_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNotificationLevelToUser < ActiveRecord::Migration def change add_column :users, :notification_level, :integer, null: false, default: 1 diff --git a/db/migrate/20130326142630_add_index_to_users_authentication_token.rb b/db/migrate/20130326142630_add_index_to_users_authentication_token.rb index d42ef113738..0592181927e 100644 --- a/db/migrate/20130326142630_add_index_to_users_authentication_token.rb +++ b/db/migrate/20130326142630_add_index_to_users_authentication_token.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexToUsersAuthenticationToken < ActiveRecord::Migration def change add_index :users, :authentication_token, unique: true diff --git a/db/migrate/20130403003950_add_last_activity_column_into_project.rb b/db/migrate/20130403003950_add_last_activity_column_into_project.rb index 85e31608d79..04a01612c6f 100644 --- a/db/migrate/20130403003950_add_last_activity_column_into_project.rb +++ b/db/migrate/20130403003950_add_last_activity_column_into_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddLastActivityColumnIntoProject < ActiveRecord::Migration def up add_column :projects, :last_activity_at, :datetime diff --git a/db/migrate/20130404164628_add_notification_level_to_user_project.rb b/db/migrate/20130404164628_add_notification_level_to_user_project.rb index 27de5d6bf55..1e072d9c6e1 100644 --- a/db/migrate/20130404164628_add_notification_level_to_user_project.rb +++ b/db/migrate/20130404164628_add_notification_level_to_user_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNotificationLevelToUserProject < ActiveRecord::Migration def change add_column :users_projects, :notification_level, :integer, null: false, default: 3 diff --git a/db/migrate/20130410175022_remove_wiki_table.rb b/db/migrate/20130410175022_remove_wiki_table.rb index 9077aa2473c..5885b1cc375 100644 --- a/db/migrate/20130410175022_remove_wiki_table.rb +++ b/db/migrate/20130410175022_remove_wiki_table.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveWikiTable < ActiveRecord::Migration def up drop_table :wikis diff --git a/db/migrate/20130419190306_allow_merges_for_forks.rb b/db/migrate/20130419190306_allow_merges_for_forks.rb index 56ea97e8561..ec953986c6a 100644 --- a/db/migrate/20130419190306_allow_merges_for_forks.rb +++ b/db/migrate/20130419190306_allow_merges_for_forks.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AllowMergesForForks < ActiveRecord::Migration def self.up add_column :merge_requests, :target_project_id, :integer, :null => true diff --git a/db/migrate/20130506085413_add_type_to_key.rb b/db/migrate/20130506085413_add_type_to_key.rb index 315e7ca77b3..c9f1ee4e389 100644 --- a/db/migrate/20130506085413_add_type_to_key.rb +++ b/db/migrate/20130506085413_add_type_to_key.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTypeToKey < ActiveRecord::Migration def change add_column :keys, :type, :string diff --git a/db/migrate/20130506090604_create_deploy_keys_projects.rb b/db/migrate/20130506090604_create_deploy_keys_projects.rb index 0dc8cdeb07d..7d6662d358a 100644 --- a/db/migrate/20130506090604_create_deploy_keys_projects.rb +++ b/db/migrate/20130506090604_create_deploy_keys_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateDeployKeysProjects < ActiveRecord::Migration def change create_table :deploy_keys_projects do |t| diff --git a/db/migrate/20130506095501_remove_project_id_from_key.rb b/db/migrate/20130506095501_remove_project_id_from_key.rb index 6b794cfb5c1..53abc4e7b52 100644 --- a/db/migrate/20130506095501_remove_project_id_from_key.rb +++ b/db/migrate/20130506095501_remove_project_id_from_key.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveProjectIdFromKey < ActiveRecord::Migration def up puts 'Migrate deploy keys: ' diff --git a/db/migrate/20130522141856_add_more_fields_to_service.rb b/db/migrate/20130522141856_add_more_fields_to_service.rb index 298e902df2f..9f764a1d050 100644 --- a/db/migrate/20130522141856_add_more_fields_to_service.rb +++ b/db/migrate/20130522141856_add_more_fields_to_service.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMoreFieldsToService < ActiveRecord::Migration def change add_column :services, :subdomain, :string diff --git a/db/migrate/20130528184641_add_system_to_notes.rb b/db/migrate/20130528184641_add_system_to_notes.rb index 1b22a4934f9..27fbf8983ac 100644 --- a/db/migrate/20130528184641_add_system_to_notes.rb +++ b/db/migrate/20130528184641_add_system_to_notes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddSystemToNotes < ActiveRecord::Migration class Note < ActiveRecord::Base end diff --git a/db/migrate/20130611210815_increase_snippet_text_column_size.rb b/db/migrate/20130611210815_increase_snippet_text_column_size.rb index f7b4447e43e..f710c79a9a5 100644 --- a/db/migrate/20130611210815_increase_snippet_text_column_size.rb +++ b/db/migrate/20130611210815_increase_snippet_text_column_size.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class IncreaseSnippetTextColumnSize < ActiveRecord::Migration def up # MYSQL LARGETEXT for snippet diff --git a/db/migrate/20130613165816_add_password_expires_at_to_users.rb b/db/migrate/20130613165816_add_password_expires_at_to_users.rb index 3479c8e64d0..47306a370a8 100644 --- a/db/migrate/20130613165816_add_password_expires_at_to_users.rb +++ b/db/migrate/20130613165816_add_password_expires_at_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPasswordExpiresAtToUsers < ActiveRecord::Migration def change add_column :users, :password_expires_at, :datetime diff --git a/db/migrate/20130613173246_add_created_by_id_to_user.rb b/db/migrate/20130613173246_add_created_by_id_to_user.rb index 615e96eb156..3138c0f40a7 100644 --- a/db/migrate/20130613173246_add_created_by_id_to_user.rb +++ b/db/migrate/20130613173246_add_created_by_id_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCreatedByIdToUser < ActiveRecord::Migration def change add_column :users, :created_by_id, :integer diff --git a/db/migrate/20130614132337_add_improted_to_project.rb b/db/migrate/20130614132337_add_improted_to_project.rb index cc882c3f10a..26dc16e3b43 100644 --- a/db/migrate/20130614132337_add_improted_to_project.rb +++ b/db/migrate/20130614132337_add_improted_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddImprotedToProject < ActiveRecord::Migration def change add_column :projects, :imported, :boolean, default: false, null: false diff --git a/db/migrate/20130617095603_create_users_groups.rb b/db/migrate/20130617095603_create_users_groups.rb index 2efc04f1151..45cff93fe4a 100644 --- a/db/migrate/20130617095603_create_users_groups.rb +++ b/db/migrate/20130617095603_create_users_groups.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateUsersGroups < ActiveRecord::Migration def change create_table :users_groups do |t| diff --git a/db/migrate/20130621195223_add_notification_level_to_user_group.rb b/db/migrate/20130621195223_add_notification_level_to_user_group.rb index 8c2e3dfcaca..6fd4941f615 100644 --- a/db/migrate/20130621195223_add_notification_level_to_user_group.rb +++ b/db/migrate/20130621195223_add_notification_level_to_user_group.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNotificationLevelToUserGroup < ActiveRecord::Migration def change add_column :users_groups, :notification_level, :integer, null: false, default: 3 diff --git a/db/migrate/20130622115340_add_more_db_index.rb b/db/migrate/20130622115340_add_more_db_index.rb index 9570a7a3f1e..4113217de59 100644 --- a/db/migrate/20130622115340_add_more_db_index.rb +++ b/db/migrate/20130622115340_add_more_db_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMoreDbIndex < ActiveRecord::Migration def change add_index :deploy_keys_projects, :project_id diff --git a/db/migrate/20130624162710_add_fingerprint_to_key.rb b/db/migrate/20130624162710_add_fingerprint_to_key.rb index 544a8366727..3e574ea81b9 100644 --- a/db/migrate/20130624162710_add_fingerprint_to_key.rb +++ b/db/migrate/20130624162710_add_fingerprint_to_key.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddFingerprintToKey < ActiveRecord::Migration def change add_column :keys, :fingerprint, :string diff --git a/db/migrate/20130711063759_create_project_group_links.rb b/db/migrate/20130711063759_create_project_group_links.rb index 395083f2a03..bd9d40a50db 100644 --- a/db/migrate/20130711063759_create_project_group_links.rb +++ b/db/migrate/20130711063759_create_project_group_links.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateProjectGroupLinks < ActiveRecord::Migration def change create_table :project_group_links do |t| diff --git a/db/migrate/20130804151314_add_st_diff_to_note.rb b/db/migrate/20130804151314_add_st_diff_to_note.rb index 3f9abb975c3..9e2da73b695 100644 --- a/db/migrate/20130804151314_add_st_diff_to_note.rb +++ b/db/migrate/20130804151314_add_st_diff_to_note.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddStDiffToNote < ActiveRecord::Migration def change add_column :notes, :st_diff, :text, :null => true diff --git a/db/migrate/20130809124851_add_permission_check_to_user.rb b/db/migrate/20130809124851_add_permission_check_to_user.rb index c26157904c7..9f9dea36101 100644 --- a/db/migrate/20130809124851_add_permission_check_to_user.rb +++ b/db/migrate/20130809124851_add_permission_check_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPermissionCheckToUser < ActiveRecord::Migration def change add_column :users, :last_credential_check_at, :datetime diff --git a/db/migrate/20130812143708_add_import_url_to_project.rb b/db/migrate/20130812143708_add_import_url_to_project.rb index 023a48741b2..d2bdfe1894e 100644 --- a/db/migrate/20130812143708_add_import_url_to_project.rb +++ b/db/migrate/20130812143708_add_import_url_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddImportUrlToProject < ActiveRecord::Migration def change add_column :projects, :import_url, :string diff --git a/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb b/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb index e55ae38f144..0e0e78b0f0d 100644 --- a/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb +++ b/db/migrate/20130819182730_add_internal_ids_to_issues_and_mr.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddInternalIdsToIssuesAndMr < ActiveRecord::Migration def change add_column :issues, :iid, :integer diff --git a/db/migrate/20130820102832_add_access_to_project_group_link.rb b/db/migrate/20130820102832_add_access_to_project_group_link.rb index 00e3947a6bb..98f3fa87523 100644 --- a/db/migrate/20130820102832_add_access_to_project_group_link.rb +++ b/db/migrate/20130820102832_add_access_to_project_group_link.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAccessToProjectGroupLink < ActiveRecord::Migration def change add_column :project_group_links, :group_access, :integer, null: false, default: ProjectGroupLink.default_access diff --git a/db/migrate/20130821090530_remove_deprecated_tables.rb b/db/migrate/20130821090530_remove_deprecated_tables.rb index 539c0617eeb..d22e713a7a1 100644 --- a/db/migrate/20130821090530_remove_deprecated_tables.rb +++ b/db/migrate/20130821090530_remove_deprecated_tables.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveDeprecatedTables < ActiveRecord::Migration def up drop_table :user_teams diff --git a/db/migrate/20130821090531_add_internal_ids_to_milestones.rb b/db/migrate/20130821090531_add_internal_ids_to_milestones.rb index 33e5bae5805..e25b8f91662 100644 --- a/db/migrate/20130821090531_add_internal_ids_to_milestones.rb +++ b/db/migrate/20130821090531_add_internal_ids_to_milestones.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddInternalIdsToMilestones < ActiveRecord::Migration def change add_column :milestones, :iid, :integer diff --git a/db/migrate/20130909132950_add_description_to_merge_request.rb b/db/migrate/20130909132950_add_description_to_merge_request.rb index 9bcd0c7ee06..fbac50c8216 100644 --- a/db/migrate/20130909132950_add_description_to_merge_request.rb +++ b/db/migrate/20130909132950_add_description_to_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDescriptionToMergeRequest < ActiveRecord::Migration def change add_column :merge_requests, :description, :text, null: true diff --git a/db/migrate/20130926081215_change_owner_id_for_group.rb b/db/migrate/20130926081215_change_owner_id_for_group.rb index 8f1992c37ab..2bdd22d5a04 100644 --- a/db/migrate/20130926081215_change_owner_id_for_group.rb +++ b/db/migrate/20130926081215_change_owner_id_for_group.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ChangeOwnerIdForGroup < ActiveRecord::Migration def up change_column :namespaces, :owner_id, :integer, null: true diff --git a/db/migrate/20131005191208_add_avatar_to_users.rb b/db/migrate/20131005191208_add_avatar_to_users.rb index 7b4de37ad72..df9057b81d6 100644 --- a/db/migrate/20131005191208_add_avatar_to_users.rb +++ b/db/migrate/20131005191208_add_avatar_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAvatarToUsers < ActiveRecord::Migration def change add_column :users, :avatar, :string diff --git a/db/migrate/20131009115346_add_confirmable_to_users.rb b/db/migrate/20131009115346_add_confirmable_to_users.rb index 249cbe704ed..d714dd98e85 100644 --- a/db/migrate/20131009115346_add_confirmable_to_users.rb +++ b/db/migrate/20131009115346_add_confirmable_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddConfirmableToUsers < ActiveRecord::Migration def self.up add_column :users, :confirmation_token, :string diff --git a/db/migrate/20131106151520_remove_default_branch.rb b/db/migrate/20131106151520_remove_default_branch.rb index 88a890eb3eb..fd3d1ed7ab3 100644 --- a/db/migrate/20131106151520_remove_default_branch.rb +++ b/db/migrate/20131106151520_remove_default_branch.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveDefaultBranch < ActiveRecord::Migration def up remove_column :projects, :default_branch diff --git a/db/migrate/20131112114325_create_broadcast_messages.rb b/db/migrate/20131112114325_create_broadcast_messages.rb index 147178e9dcf..ce37a8e2708 100644 --- a/db/migrate/20131112114325_create_broadcast_messages.rb +++ b/db/migrate/20131112114325_create_broadcast_messages.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateBroadcastMessages < ActiveRecord::Migration def change create_table :broadcast_messages do |t| diff --git a/db/migrate/20131112220935_add_visibility_level_to_projects.rb b/db/migrate/20131112220935_add_visibility_level_to_projects.rb index 89421cbedad..5efc17b228e 100644 --- a/db/migrate/20131112220935_add_visibility_level_to_projects.rb +++ b/db/migrate/20131112220935_add_visibility_level_to_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddVisibilityLevelToProjects < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20131129154016_add_archived_to_projects.rb b/db/migrate/20131129154016_add_archived_to_projects.rb index 917e690ba47..e8e6908d137 100644 --- a/db/migrate/20131129154016_add_archived_to_projects.rb +++ b/db/migrate/20131129154016_add_archived_to_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddArchivedToProjects < ActiveRecord::Migration def change add_column :projects, :archived, :boolean, default: false, null: false diff --git a/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb b/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb index 473f355eceb..348a284a53e 100644 --- a/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb +++ b/db/migrate/20131130165425_add_color_and_font_to_broadcast_messages.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddColorAndFontToBroadcastMessages < ActiveRecord::Migration def change add_column :broadcast_messages, :color, :string diff --git a/db/migrate/20131202192556_add_event_fields_for_web_hook.rb b/db/migrate/20131202192556_add_event_fields_for_web_hook.rb index d29e996852e..99d76611524 100644 --- a/db/migrate/20131202192556_add_event_fields_for_web_hook.rb +++ b/db/migrate/20131202192556_add_event_fields_for_web_hook.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddEventFieldsForWebHook < ActiveRecord::Migration def change add_column :web_hooks, :push_events, :boolean, default: true, null: false diff --git a/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb b/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb index 7cec79e7ee8..4333dc59323 100644 --- a/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb +++ b/db/migrate/20131214224427_add_hide_no_ssh_key_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddHideNoSshKeyToUsers < ActiveRecord::Migration def change add_column :users, :hide_no_ssh_key, :boolean, :default => false diff --git a/db/migrate/20131217102743_add_recipients_to_service.rb b/db/migrate/20131217102743_add_recipients_to_service.rb index 9695c251352..3c76be0f68d 100644 --- a/db/migrate/20131217102743_add_recipients_to_service.rb +++ b/db/migrate/20131217102743_add_recipients_to_service.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRecipientsToService < ActiveRecord::Migration def change add_column :services, :recipients, :text diff --git a/db/migrate/20140116231608_add_website_url_to_users.rb b/db/migrate/20140116231608_add_website_url_to_users.rb index 0996fdcad73..1c39423562e 100644 --- a/db/migrate/20140116231608_add_website_url_to_users.rb +++ b/db/migrate/20140116231608_add_website_url_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddWebsiteUrlToUsers < ActiveRecord::Migration def change add_column :users, :website_url, :string, {:null => false, :default => ''} diff --git a/db/migrate/20140122112253_create_merge_request_diffs.rb b/db/migrate/20140122112253_create_merge_request_diffs.rb index f34e30925df..395c3edfc79 100644 --- a/db/migrate/20140122112253_create_merge_request_diffs.rb +++ b/db/migrate/20140122112253_create_merge_request_diffs.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateMergeRequestDiffs < ActiveRecord::Migration def up create_table :merge_request_diffs do |t| diff --git a/db/migrate/20140122114406_migrate_mr_diffs.rb b/db/migrate/20140122114406_migrate_mr_diffs.rb index 1595e2b6472..429aeb2293f 100644 --- a/db/migrate/20140122114406_migrate_mr_diffs.rb +++ b/db/migrate/20140122114406_migrate_mr_diffs.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateMrDiffs < ActiveRecord::Migration def self.up execute "INSERT INTO merge_request_diffs ( merge_request_id, st_commits, st_diffs ) SELECT id, st_commits, st_diffs FROM merge_requests" diff --git a/db/migrate/20140122122549_remove_m_rdiff_fields.rb b/db/migrate/20140122122549_remove_m_rdiff_fields.rb index 8f863d85a68..bbf35811b61 100644 --- a/db/migrate/20140122122549_remove_m_rdiff_fields.rb +++ b/db/migrate/20140122122549_remove_m_rdiff_fields.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveMRdiffFields < ActiveRecord::Migration def up remove_column :merge_requests, :st_commits diff --git a/db/migrate/20140125162722_add_avatar_to_projects.rb b/db/migrate/20140125162722_add_avatar_to_projects.rb index 9523ac722f2..888341b7535 100644 --- a/db/migrate/20140125162722_add_avatar_to_projects.rb +++ b/db/migrate/20140125162722_add_avatar_to_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAvatarToProjects < ActiveRecord::Migration def change add_column :projects, :avatar, :string diff --git a/db/migrate/20140127170938_add_group_avatars.rb b/db/migrate/20140127170938_add_group_avatars.rb index 2911096dd5d..95d1c1c6b27 100644 --- a/db/migrate/20140127170938_add_group_avatars.rb +++ b/db/migrate/20140127170938_add_group_avatars.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddGroupAvatars < ActiveRecord::Migration def change add_column :namespaces, :avatar, :string diff --git a/db/migrate/20140209025651_create_emails.rb b/db/migrate/20140209025651_create_emails.rb index cb78c4af11b..571beb19cdd 100644 --- a/db/migrate/20140209025651_create_emails.rb +++ b/db/migrate/20140209025651_create_emails.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateEmails < ActiveRecord::Migration def change create_table :emails do |t| diff --git a/db/migrate/20140214102325_add_api_key_to_services.rb b/db/migrate/20140214102325_add_api_key_to_services.rb index 30eeca2c1f6..b58c36c0a30 100644 --- a/db/migrate/20140214102325_add_api_key_to_services.rb +++ b/db/migrate/20140214102325_add_api_key_to_services.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddApiKeyToServices < ActiveRecord::Migration def change add_column :services, :api_key, :string diff --git a/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb b/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb index 65d28e8cb01..aab8a41c2c3 100644 --- a/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb +++ b/db/migrate/20140304005354_add_index_merge_request_diffs_on_merge_request_id.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexMergeRequestDiffsOnMergeRequestId < ActiveRecord::Migration def change add_index :merge_request_diffs, :merge_request_id, unique: true diff --git a/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb b/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb index 7017148702a..ec163bb843c 100644 --- a/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb +++ b/db/migrate/20140305193308_add_tag_push_hooks_to_project_hook.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTagPushHooksToProjectHook < ActiveRecord::Migration def change add_column :web_hooks, :tag_push_events, :boolean, default: false diff --git a/db/migrate/20140312145357_add_import_status_to_project.rb b/db/migrate/20140312145357_add_import_status_to_project.rb index ef972e8342a..9947cd8c6f9 100644 --- a/db/migrate/20140312145357_add_import_status_to_project.rb +++ b/db/migrate/20140312145357_add_import_status_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddImportStatusToProject < ActiveRecord::Migration def change add_column :projects, :import_status, :string diff --git a/db/migrate/20140313092127_migrate_already_imported_projects.rb b/db/migrate/20140313092127_migrate_already_imported_projects.rb index 0a9f73a5758..f2e91fe1b40 100644 --- a/db/migrate/20140313092127_migrate_already_imported_projects.rb +++ b/db/migrate/20140313092127_migrate_already_imported_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateAlreadyImportedProjects < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20140407135544_fix_namespaces.rb b/db/migrate/20140407135544_fix_namespaces.rb index 59665d538f0..91374966698 100644 --- a/db/migrate/20140407135544_fix_namespaces.rb +++ b/db/migrate/20140407135544_fix_namespaces.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class FixNamespaces < ActiveRecord::Migration def up Namespace.where('name <> path and type is null').each do |namespace| diff --git a/db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb b/db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb index 1f6d85d5f66..fb9c7a6636e 100644 --- a/db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb +++ b/db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ChangeStateToAllowEmptyMergeRequestDiffs < ActiveRecord::Migration def up change_column :merge_request_diffs, :state, :string, null: true, diff --git a/db/migrate/20140415124820_limits_to_mysql.rb b/db/migrate/20140415124820_limits_to_mysql.rb index 3f6e62617c5..c712423bcd1 100644 --- a/db/migrate/20140415124820_limits_to_mysql.rb +++ b/db/migrate/20140415124820_limits_to_mysql.rb @@ -1 +1,2 @@ +# rubocop:disable all require_relative 'limits_to_mysql' diff --git a/db/migrate/20140416074002_add_index_on_iid.rb b/db/migrate/20140416074002_add_index_on_iid.rb index 85269e2a03b..6cdaa5a3c08 100644 --- a/db/migrate/20140416074002_add_index_on_iid.rb +++ b/db/migrate/20140416074002_add_index_on_iid.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexOnIid < ActiveRecord::Migration def change RemoveDuplicateIid.clean(Issue) diff --git a/db/migrate/20140416185734_index_on_current_sign_in_at.rb b/db/migrate/20140416185734_index_on_current_sign_in_at.rb index 0bf80ce154a..8c620b545bd 100644 --- a/db/migrate/20140416185734_index_on_current_sign_in_at.rb +++ b/db/migrate/20140416185734_index_on_current_sign_in_at.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class IndexOnCurrentSignInAt < ActiveRecord::Migration def change add_index :users, :current_sign_in_at diff --git a/db/migrate/20140428105831_add_notes_index_updated_at.rb b/db/migrate/20140428105831_add_notes_index_updated_at.rb index 6c25570f128..0589101af93 100644 --- a/db/migrate/20140428105831_add_notes_index_updated_at.rb +++ b/db/migrate/20140428105831_add_notes_index_updated_at.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNotesIndexUpdatedAt < ActiveRecord::Migration def change add_index :notes, :updated_at diff --git a/db/migrate/20140502115131_add_repo_size_to_db.rb b/db/migrate/20140502115131_add_repo_size_to_db.rb index 7361d1a9440..090b30a4f26 100644 --- a/db/migrate/20140502115131_add_repo_size_to_db.rb +++ b/db/migrate/20140502115131_add_repo_size_to_db.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRepoSizeToDb < ActiveRecord::Migration def change add_column :projects, :repository_size, :float, default: 0 diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb index efdf53112fd..84463727b3b 100644 --- a/db/migrate/20140502125220_migrate_repo_size.rb +++ b/db/migrate/20140502125220_migrate_repo_size.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateRepoSize < ActiveRecord::Migration def up project_data = execute('SELECT projects.id, namespaces.path AS namespace_path, projects.path AS project_path FROM projects LEFT JOIN namespaces ON projects.namespace_id = namespaces.id') diff --git a/db/migrate/20140611135229_add_position_to_merge_request.rb b/db/migrate/20140611135229_add_position_to_merge_request.rb index d5fdecd0c39..3a7d2f7c359 100644 --- a/db/migrate/20140611135229_add_position_to_merge_request.rb +++ b/db/migrate/20140611135229_add_position_to_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPositionToMergeRequest < ActiveRecord::Migration def change add_column :merge_requests, :position, :integer, default: 0 diff --git a/db/migrate/20140625115202_create_users_star_projects.rb b/db/migrate/20140625115202_create_users_star_projects.rb index 412f0f6f34b..32dd99e83be 100644 --- a/db/migrate/20140625115202_create_users_star_projects.rb +++ b/db/migrate/20140625115202_create_users_star_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateUsersStarProjects < ActiveRecord::Migration def change create_table :users_star_projects do |t| diff --git a/db/migrate/20140729134820_create_labels.rb b/db/migrate/20140729134820_create_labels.rb index 3a4b6a152dc..df0f8cb9f03 100644 --- a/db/migrate/20140729134820_create_labels.rb +++ b/db/migrate/20140729134820_create_labels.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateLabels < ActiveRecord::Migration def change create_table :labels do |t| diff --git a/db/migrate/20140729140420_create_label_links.rb b/db/migrate/20140729140420_create_label_links.rb index 2bfc4ae2094..fa5992605f8 100644 --- a/db/migrate/20140729140420_create_label_links.rb +++ b/db/migrate/20140729140420_create_label_links.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateLabelLinks < ActiveRecord::Migration def change create_table :label_links do |t| diff --git a/db/migrate/20140729145339_migrate_project_tags.rb b/db/migrate/20140729145339_migrate_project_tags.rb index 5760e4bfeaa..ac46847f3e6 100644 --- a/db/migrate/20140729145339_migrate_project_tags.rb +++ b/db/migrate/20140729145339_migrate_project_tags.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateProjectTags < ActiveRecord::Migration def up ActsAsTaggableOn::Tagging.where(taggable_type: 'Project', context: 'labels').update_all(context: 'tags') diff --git a/db/migrate/20140729152420_migrate_taggable_labels.rb b/db/migrate/20140729152420_migrate_taggable_labels.rb index dc28d727d9a..04cdc6beadd 100644 --- a/db/migrate/20140729152420_migrate_taggable_labels.rb +++ b/db/migrate/20140729152420_migrate_taggable_labels.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateTaggableLabels < ActiveRecord::Migration def up taggings = ActsAsTaggableOn::Tagging.where(taggable_type: ['Issue', 'MergeRequest'], context: 'labels') diff --git a/db/migrate/20140730111702_add_index_to_labels.rb b/db/migrate/20140730111702_add_index_to_labels.rb index 494241c873c..cc7ac1fc449 100644 --- a/db/migrate/20140730111702_add_index_to_labels.rb +++ b/db/migrate/20140730111702_add_index_to_labels.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexToLabels < ActiveRecord::Migration def change add_index "labels", :project_id diff --git a/db/migrate/20140903115954_migrate_to_new_shell.rb b/db/migrate/20140903115954_migrate_to_new_shell.rb index 54cbe48960a..04acf24284b 100644 --- a/db/migrate/20140903115954_migrate_to_new_shell.rb +++ b/db/migrate/20140903115954_migrate_to_new_shell.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateToNewShell < ActiveRecord::Migration def change return if Rails.env.test? diff --git a/db/migrate/20140907220153_serialize_service_properties.rb b/db/migrate/20140907220153_serialize_service_properties.rb index d45a10465be..c2d67fad0ab 100644 --- a/db/migrate/20140907220153_serialize_service_properties.rb +++ b/db/migrate/20140907220153_serialize_service_properties.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class SerializeServiceProperties < ActiveRecord::Migration def change unless column_exists?(:services, :properties) diff --git a/db/migrate/20140914113604_add_members_table.rb b/db/migrate/20140914113604_add_members_table.rb index d311f3033ee..bc3c1bb61e4 100644 --- a/db/migrate/20140914113604_add_members_table.rb +++ b/db/migrate/20140914113604_add_members_table.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMembersTable < ActiveRecord::Migration def change create_table :members do |t| diff --git a/db/migrate/20140914145549_migrate_to_new_members_model.rb b/db/migrate/20140914145549_migrate_to_new_members_model.rb index 2a5a49c724a..b4c98f016d0 100644 --- a/db/migrate/20140914145549_migrate_to_new_members_model.rb +++ b/db/migrate/20140914145549_migrate_to_new_members_model.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateToNewMembersModel < ActiveRecord::Migration def up execute "INSERT INTO members ( user_id, source_id, source_type, access_level, notification_level, type ) SELECT user_id, group_id, 'Namespace', group_access, notification_level, 'GroupMember' FROM users_groups" diff --git a/db/migrate/20140914173417_remove_old_member_tables.rb b/db/migrate/20140914173417_remove_old_member_tables.rb index 408b9551dbb..aff8e94e5be 100644 --- a/db/migrate/20140914173417_remove_old_member_tables.rb +++ b/db/migrate/20140914173417_remove_old_member_tables.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveOldMemberTables < ActiveRecord::Migration def up drop_table :users_groups diff --git a/db/migrate/20141006143943_move_slack_service_to_webhook.rb b/db/migrate/20141006143943_move_slack_service_to_webhook.rb index 5836cd6b8db..8cb120f7007 100644 --- a/db/migrate/20141006143943_move_slack_service_to_webhook.rb +++ b/db/migrate/20141006143943_move_slack_service_to_webhook.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MoveSlackServiceToWebhook < ActiveRecord::Migration def change SlackService.all.each do |slack_service| diff --git a/db/migrate/20141007100818_add_visibility_level_to_snippet.rb b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb index 93826185e8b..688d8578478 100644 --- a/db/migrate/20141007100818_add_visibility_level_to_snippet.rb +++ b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddVisibilityLevelToSnippet < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20141118150935_add_audit_event.rb b/db/migrate/20141118150935_add_audit_event.rb index 07383c6bbc7..3884228456f 100644 --- a/db/migrate/20141118150935_add_audit_event.rb +++ b/db/migrate/20141118150935_add_audit_event.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAuditEvent < ActiveRecord::Migration def change create_table :audit_events do |t| diff --git a/db/migrate/20141121133009_add_timestamps_to_members.rb b/db/migrate/20141121133009_add_timestamps_to_members.rb index ef6d4dedf32..68f164cd35d 100644 --- a/db/migrate/20141121133009_add_timestamps_to_members.rb +++ b/db/migrate/20141121133009_add_timestamps_to_members.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # In 20140914145549_migrate_to_new_members_model.rb we forgot to set the # created_at and updated_at times for new records in the 'members' table. This # became a problem after commit c8e78d972a5a628870eefca0f2ccea0199c55bda which diff --git a/db/migrate/20141121161704_add_identity_table.rb b/db/migrate/20141121161704_add_identity_table.rb index a85b0426cec..5a399f0d325 100644 --- a/db/migrate/20141121161704_add_identity_table.rb +++ b/db/migrate/20141121161704_add_identity_table.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIdentityTable < ActiveRecord::Migration def up create_table :identities do |t| diff --git a/db/migrate/20141205134006_add_locked_at_to_merge_request.rb b/db/migrate/20141205134006_add_locked_at_to_merge_request.rb index 49651c44a82..5aa91c7587a 100644 --- a/db/migrate/20141205134006_add_locked_at_to_merge_request.rb +++ b/db/migrate/20141205134006_add_locked_at_to_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddLockedAtToMergeRequest < ActiveRecord::Migration def change add_column :merge_requests, :locked_at, :datetime diff --git a/db/migrate/20141216155758_create_doorkeeper_tables.rb b/db/migrate/20141216155758_create_doorkeeper_tables.rb index af5aa7d8b73..b323ffe96f5 100644 --- a/db/migrate/20141216155758_create_doorkeeper_tables.rb +++ b/db/migrate/20141216155758_create_doorkeeper_tables.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateDoorkeeperTables < ActiveRecord::Migration def change create_table :oauth_applications do |t| diff --git a/db/migrate/20141217125223_add_owner_to_application.rb b/db/migrate/20141217125223_add_owner_to_application.rb index 7d5e6d07d0f..e5a669ab4d8 100644 --- a/db/migrate/20141217125223_add_owner_to_application.rb +++ b/db/migrate/20141217125223_add_owner_to_application.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddOwnerToApplication < ActiveRecord::Migration def change add_column :oauth_applications, :owner_id, :integer, null: true diff --git a/db/migrate/20141223135007_add_import_data_to_project_table.rb b/db/migrate/20141223135007_add_import_data_to_project_table.rb index 5db78f94cc9..9c8a483e4d5 100644 --- a/db/migrate/20141223135007_add_import_data_to_project_table.rb +++ b/db/migrate/20141223135007_add_import_data_to_project_table.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddImportDataToProjectTable < ActiveRecord::Migration def change add_column :projects, :import_type, :string diff --git a/db/migrate/20141226080412_add_developers_can_push_to_protected_branches.rb b/db/migrate/20141226080412_add_developers_can_push_to_protected_branches.rb index 70e7272f7f3..a18b2f4974d 100644 --- a/db/migrate/20141226080412_add_developers_can_push_to_protected_branches.rb +++ b/db/migrate/20141226080412_add_developers_can_push_to_protected_branches.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDevelopersCanPushToProtectedBranches < ActiveRecord::Migration def change add_column :protected_branches, :developers_can_push, :boolean, default: false, null: false diff --git a/db/migrate/20150108073740_create_application_settings.rb b/db/migrate/20150108073740_create_application_settings.rb index 651e35fdf7a..dfa2f765357 100644 --- a/db/migrate/20150108073740_create_application_settings.rb +++ b/db/migrate/20150108073740_create_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateApplicationSettings < ActiveRecord::Migration def change create_table :application_settings do |t| diff --git a/db/migrate/20150116234544_add_home_page_url_for_application_settings.rb b/db/migrate/20150116234544_add_home_page_url_for_application_settings.rb index aa179ce3a4d..10e6549c729 100644 --- a/db/migrate/20150116234544_add_home_page_url_for_application_settings.rb +++ b/db/migrate/20150116234544_add_home_page_url_for_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddHomePageUrlForApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :home_page_url, :string diff --git a/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb b/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb index c28ba3197ac..e083973615a 100644 --- a/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb +++ b/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddGitlabAccessTokenToUser < ActiveRecord::Migration def change add_column :users, :gitlab_access_token, :string diff --git a/db/migrate/20150125163100_add_default_branch_protection_setting.rb b/db/migrate/20150125163100_add_default_branch_protection_setting.rb index 5020daf55f3..7ca3116d354 100644 --- a/db/migrate/20150125163100_add_default_branch_protection_setting.rb +++ b/db/migrate/20150125163100_add_default_branch_protection_setting.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDefaultBranchProtectionSetting < ActiveRecord::Migration def change add_column :application_settings, :default_branch_protection, :integer, :default => 2 diff --git a/db/migrate/20150205211843_add_timestamps_to_identities.rb b/db/migrate/20150205211843_add_timestamps_to_identities.rb index 77cddbfec3b..a78e28eb4eb 100644 --- a/db/migrate/20150205211843_add_timestamps_to_identities.rb +++ b/db/migrate/20150205211843_add_timestamps_to_identities.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTimestampsToIdentities < ActiveRecord::Migration def change add_timestamps(:identities) diff --git a/db/migrate/20150206181414_add_index_to_created_at.rb b/db/migrate/20150206181414_add_index_to_created_at.rb index fc624fca60d..a161fad79dc 100644 --- a/db/migrate/20150206181414_add_index_to_created_at.rb +++ b/db/migrate/20150206181414_add_index_to_created_at.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexToCreatedAt < ActiveRecord::Migration def change add_index "users", [:created_at, :id] diff --git a/db/migrate/20150206222854_add_notification_email_to_user.rb b/db/migrate/20150206222854_add_notification_email_to_user.rb index ab80f7e582f..ebae092cac8 100644 --- a/db/migrate/20150206222854_add_notification_email_to_user.rb +++ b/db/migrate/20150206222854_add_notification_email_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNotificationEmailToUser < ActiveRecord::Migration def up add_column :users, :notification_email, :string diff --git a/db/migrate/20150209222013_add_missing_index.rb b/db/migrate/20150209222013_add_missing_index.rb index a816c2e9e8c..18e3ac2cbbb 100644 --- a/db/migrate/20150209222013_add_missing_index.rb +++ b/db/migrate/20150209222013_add_missing_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMissingIndex < ActiveRecord::Migration def change add_index "services", [:created_at, :id] diff --git a/db/migrate/20150211172122_add_template_to_service.rb b/db/migrate/20150211172122_add_template_to_service.rb index b1bfbc45ee9..a3e96b25c56 100644 --- a/db/migrate/20150211172122_add_template_to_service.rb +++ b/db/migrate/20150211172122_add_template_to_service.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTemplateToService < ActiveRecord::Migration def change add_column :services, :template, :boolean, default: false diff --git a/db/migrate/20150211174341_allow_null_in_services_project_id.rb b/db/migrate/20150211174341_allow_null_in_services_project_id.rb index 68f02812791..fea95c79adf 100644 --- a/db/migrate/20150211174341_allow_null_in_services_project_id.rb +++ b/db/migrate/20150211174341_allow_null_in_services_project_id.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AllowNullInServicesProjectId < ActiveRecord::Migration def change change_column :services, :project_id, :integer, null: true diff --git a/db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb b/db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb index a0439172391..334020376e4 100644 --- a/db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb +++ b/db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTwitterSharingEnabledToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :twitter_sharing_enabled, :boolean, default: true diff --git a/db/migrate/20150213114800_add_hide_no_password_to_user.rb b/db/migrate/20150213114800_add_hide_no_password_to_user.rb index 685f0844276..a2af3510b9c 100644 --- a/db/migrate/20150213114800_add_hide_no_password_to_user.rb +++ b/db/migrate/20150213114800_add_hide_no_password_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddHideNoPasswordToUser < ActiveRecord::Migration def change add_column :users, :hide_no_password, :boolean, default: false diff --git a/db/migrate/20150213121042_add_password_automatically_set_to_user.rb b/db/migrate/20150213121042_add_password_automatically_set_to_user.rb index c3c7c1ffc77..4e84a13f0d2 100644 --- a/db/migrate/20150213121042_add_password_automatically_set_to_user.rb +++ b/db/migrate/20150213121042_add_password_automatically_set_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPasswordAutomaticallySetToUser < ActiveRecord::Migration def change add_column :users, :password_automatically_set, :boolean, default: false diff --git a/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb b/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb index 23ac1b399ec..78e9fd0c3a9 100644 --- a/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb +++ b/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddBitbucketAccessTokenAndSecretToUser < ActiveRecord::Migration def change add_column :users, :bitbucket_access_token, :string diff --git a/db/migrate/20150219004514_add_events_to_services.rb b/db/migrate/20150219004514_add_events_to_services.rb index cf73a0174f4..560382c3fa1 100644 --- a/db/migrate/20150219004514_add_events_to_services.rb +++ b/db/migrate/20150219004514_add_events_to_services.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddEventsToServices < ActiveRecord::Migration def change add_column :services, :push_events, :boolean, :default => true diff --git a/db/migrate/20150223022001_set_missing_last_activity_at.rb b/db/migrate/20150223022001_set_missing_last_activity_at.rb index 3f6d4d83474..300381ad65b 100644 --- a/db/migrate/20150223022001_set_missing_last_activity_at.rb +++ b/db/migrate/20150223022001_set_missing_last_activity_at.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class SetMissingLastActivityAt < ActiveRecord::Migration def up execute "UPDATE projects SET last_activity_at = updated_at WHERE last_activity_at IS NULL" diff --git a/db/migrate/20150225065047_add_note_events_to_services.rb b/db/migrate/20150225065047_add_note_events_to_services.rb index d54ba9e482f..7843cabc43b 100644 --- a/db/migrate/20150225065047_add_note_events_to_services.rb +++ b/db/migrate/20150225065047_add_note_events_to_services.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNoteEventsToServices < ActiveRecord::Migration def change add_column :services, :note_events, :boolean, default: true, null: false diff --git a/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb b/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb index 494c3033bff..7d8d65ef2ee 100644 --- a/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb +++ b/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRestrictedVisibilityLevelsToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :restricted_visibility_levels, :text diff --git a/db/migrate/20150306023106_fix_namespace_duplication.rb b/db/migrate/20150306023106_fix_namespace_duplication.rb index 334e5574559..ea53a9d71f2 100644 --- a/db/migrate/20150306023106_fix_namespace_duplication.rb +++ b/db/migrate/20150306023106_fix_namespace_duplication.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class FixNamespaceDuplication < ActiveRecord::Migration def up #fixes path duplication diff --git a/db/migrate/20150306023112_add_unique_index_to_namespace.rb b/db/migrate/20150306023112_add_unique_index_to_namespace.rb index 6472138e3ef..f293a9b643f 100644 --- a/db/migrate/20150306023112_add_unique_index_to_namespace.rb +++ b/db/migrate/20150306023112_add_unique_index_to_namespace.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUniqueIndexToNamespace < ActiveRecord::Migration def change remove_index :namespaces, column: :name if index_exists?(:namespaces, :name) diff --git a/db/migrate/20150310194358_add_version_check_to_application_settings.rb b/db/migrate/20150310194358_add_version_check_to_application_settings.rb index e9d42c1e749..5d3dae6e7d8 100644 --- a/db/migrate/20150310194358_add_version_check_to_application_settings.rb +++ b/db/migrate/20150310194358_add_version_check_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddVersionCheckToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :version_check_enabled, :boolean, default: true diff --git a/db/migrate/20150313012111_create_subscriptions_table.rb b/db/migrate/20150313012111_create_subscriptions_table.rb index a1d4d9dedc5..8adb193b27f 100644 --- a/db/migrate/20150313012111_create_subscriptions_table.rb +++ b/db/migrate/20150313012111_create_subscriptions_table.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateSubscriptionsTable < ActiveRecord::Migration def change create_table :subscriptions do |t| diff --git a/db/migrate/20150320234437_add_location_to_user.rb b/db/migrate/20150320234437_add_location_to_user.rb index 32731d37d75..df046570361 100644 --- a/db/migrate/20150320234437_add_location_to_user.rb +++ b/db/migrate/20150320234437_add_location_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddLocationToUser < ActiveRecord::Migration def change add_column :users, :location, :string diff --git a/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb b/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb index 42dc8173e46..9f8b6f4bd59 100644 --- a/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb +++ b/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class SetIncorrectAssigneeIdToNull < ActiveRecord::Migration def up execute "UPDATE issues SET assignee_id = NULL WHERE assignee_id = -1" diff --git a/db/migrate/20150327122227_add_public_to_key.rb b/db/migrate/20150327122227_add_public_to_key.rb index 6ffbf4cda19..33c20d65e03 100644 --- a/db/migrate/20150327122227_add_public_to_key.rb +++ b/db/migrate/20150327122227_add_public_to_key.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPublicToKey < ActiveRecord::Migration def change add_column :keys, :public, :boolean, default: false, null: false diff --git a/db/migrate/20150327150017_add_import_data_to_project.rb b/db/migrate/20150327150017_add_import_data_to_project.rb index 12c00339eec..67b1554dfd1 100644 --- a/db/migrate/20150327150017_add_import_data_to_project.rb +++ b/db/migrate/20150327150017_add_import_data_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddImportDataToProject < ActiveRecord::Migration def change add_column :projects, :import_data, :text diff --git a/db/migrate/20150327223628_add_devise_two_factor_to_users.rb b/db/migrate/20150327223628_add_devise_two_factor_to_users.rb index 11b026ee8f3..eccb0123e77 100644 --- a/db/migrate/20150327223628_add_devise_two_factor_to_users.rb +++ b/db/migrate/20150327223628_add_devise_two_factor_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDeviseTwoFactorToUsers < ActiveRecord::Migration def change add_column :users, :encrypted_otp_secret, :string diff --git a/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb b/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb index 1d161674a9a..4c56a2fb78b 100644 --- a/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb +++ b/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMaxAttachmentSizeToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :max_attachment_size, :integer, default: 10, null: false diff --git a/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb b/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb index 913958db7c5..fdb6d72917e 100644 --- a/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb +++ b/db/migrate/20150331183602_add_devise_two_factor_backupable_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration def change add_column :users, :otp_backup_codes, :text diff --git a/db/migrate/20150406133311_add_invite_data_to_member.rb b/db/migrate/20150406133311_add_invite_data_to_member.rb index 5d3e856ddce..63d0f184f32 100644 --- a/db/migrate/20150406133311_add_invite_data_to_member.rb +++ b/db/migrate/20150406133311_add_invite_data_to_member.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddInviteDataToMember < ActiveRecord::Migration def up add_column :members, :created_by_id, :integer diff --git a/db/migrate/20150411000035_fix_identities.rb b/db/migrate/20150411000035_fix_identities.rb index d9051f9fffd..a10fcc001f4 100644 --- a/db/migrate/20150411000035_fix_identities.rb +++ b/db/migrate/20150411000035_fix_identities.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class FixIdentities < ActiveRecord::Migration def up # Up until now, legacy 'ldap' references in the database were charitably diff --git a/db/migrate/20150411180045_rename_buildbox_service.rb b/db/migrate/20150411180045_rename_buildbox_service.rb index 5a0b5d07e50..9f3b25c3971 100644 --- a/db/migrate/20150411180045_rename_buildbox_service.rb +++ b/db/migrate/20150411180045_rename_buildbox_service.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RenameBuildboxService < ActiveRecord::Migration def up execute "UPDATE services SET type = 'BuildkiteService' WHERE type = 'BuildboxService';" diff --git a/db/migrate/20150413192223_add_public_email_to_users.rb b/db/migrate/20150413192223_add_public_email_to_users.rb index 700e9f343a6..0fed5eaf461 100644 --- a/db/migrate/20150413192223_add_public_email_to_users.rb +++ b/db/migrate/20150413192223_add_public_email_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPublicEmailToUsers < ActiveRecord::Migration def change add_column :users, :public_email, :string, default: "", null: false diff --git a/db/migrate/20150417121913_create_project_import_data.rb b/db/migrate/20150417121913_create_project_import_data.rb index c78f5fde85e..fc357cbacc8 100644 --- a/db/migrate/20150417121913_create_project_import_data.rb +++ b/db/migrate/20150417121913_create_project_import_data.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateProjectImportData < ActiveRecord::Migration def change create_table :project_import_data do |t| diff --git a/db/migrate/20150417122318_remove_import_data_from_project.rb b/db/migrate/20150417122318_remove_import_data_from_project.rb index 46cf63593c9..5a008218fa5 100644 --- a/db/migrate/20150417122318_remove_import_data_from_project.rb +++ b/db/migrate/20150417122318_remove_import_data_from_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveImportDataFromProject < ActiveRecord::Migration def up remove_column :projects, :import_data diff --git a/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb b/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb index 3057ea3c68c..3445e9ce59e 100644 --- a/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb +++ b/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemovePeriodsAtEndsOfUsernames < ActiveRecord::Migration include Gitlab::ShellAdapter diff --git a/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb b/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb index 50a9b2439e0..129ce4d04af 100644 --- a/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb +++ b/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDefaultProjectVisibililtyToApplicationSettings < ActiveRecord::Migration def up add_column :application_settings, :default_project_visibility, :integer diff --git a/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb b/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb index 281c88d2a7d..8f352414ffd 100644 --- a/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb +++ b/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # This migration is a duplicate of 20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb # It shold be applied before the index additions to ensure that `name` is case sensitive. diff --git a/db/migrate/20150425164647_remove_duplicate_tags.rb b/db/migrate/20150425164647_remove_duplicate_tags.rb index 13e5038db9c..e77623bf507 100644 --- a/db/migrate/20150425164647_remove_duplicate_tags.rb +++ b/db/migrate/20150425164647_remove_duplicate_tags.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveDuplicateTags < ActiveRecord::Migration def up select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag| diff --git a/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb b/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb index c1b78681519..cbff98cdbc4 100644 --- a/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb +++ b/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # This migration comes from acts_as_taggable_on_engine (originally 2) class AddMissingUniqueIndices < ActiveRecord::Migration def self.up diff --git a/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb b/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb index 8edb5080781..1568d2dd4ce 100644 --- a/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb +++ b/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # This migration comes from acts_as_taggable_on_engine (originally 3) class AddTaggingsCounterCacheToTags < ActiveRecord::Migration def self.up diff --git a/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb b/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb index 71f2d7f4330..88829b87711 100644 --- a/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb +++ b/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # This migration comes from acts_as_taggable_on_engine (originally 4) class AddMissingTaggableIndex < ActiveRecord::Migration def self.up diff --git a/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb b/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb index bfb06bc7cda..642c4745321 100644 --- a/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb +++ b/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # This migration comes from acts_as_taggable_on_engine (originally 5) # This migration is added to circumvent issue #623 and have special characters # work properly diff --git a/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb b/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb index 8f1b0cc8935..dd13def4176 100644 --- a/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb +++ b/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDefaultSnippetVisibilityToAppSettings < ActiveRecord::Migration def up add_column :application_settings, :default_snippet_visibility, :integer diff --git a/db/migrate/20150429002313_remove_abandoned_group_members_records.rb b/db/migrate/20150429002313_remove_abandoned_group_members_records.rb index 244637e1c4a..d2c7f3c442e 100644 --- a/db/migrate/20150429002313_remove_abandoned_group_members_records.rb +++ b/db/migrate/20150429002313_remove_abandoned_group_members_records.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveAbandonedGroupMembersRecords < ActiveRecord::Migration def up execute("DELETE FROM members WHERE type = 'GroupMember' AND source_id NOT IN(\ diff --git a/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb b/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb index 184e2653610..b63ea9aec7a 100644 --- a/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb +++ b/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRestrictedSignupDomainsToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :restricted_signup_domains, :text diff --git a/db/migrate/20150509180749_convert_legacy_reference_notes.rb b/db/migrate/20150509180749_convert_legacy_reference_notes.rb index b02605489be..cd8bf90108d 100644 --- a/db/migrate/20150509180749_convert_legacy_reference_notes.rb +++ b/db/migrate/20150509180749_convert_legacy_reference_notes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # Convert legacy Markdown-emphasized notes to the current, non-emphasized format # # _mentioned in 54f7727c850972f0401c1312a7c4a6a380de5666_ diff --git a/db/migrate/20150516060434_add_note_events_to_web_hooks.rb b/db/migrate/20150516060434_add_note_events_to_web_hooks.rb index 0097587b4f6..bf72e5e2e3a 100644 --- a/db/migrate/20150516060434_add_note_events_to_web_hooks.rb +++ b/db/migrate/20150516060434_add_note_events_to_web_hooks.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNoteEventsToWebHooks < ActiveRecord::Migration def up add_column :web_hooks, :note_events, :boolean, default: false, null: false diff --git a/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb b/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb index 6a78294f0b2..9b02eda56ab 100644 --- a/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb +++ b/db/migrate/20150529111607_add_user_oauth_applications_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUserOauthApplicationsToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :user_oauth_applications, :bool, default: true diff --git a/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb b/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb index 83e08101407..833c36de52d 100644 --- a/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb +++ b/db/migrate/20150529150354_add_after_sign_out_path_for_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAfterSignOutPathForApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :after_sign_out_path, :string diff --git a/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb b/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb index 61ff0af41f4..1f5cf1fe5f1 100644 --- a/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb +++ b/db/migrate/20150609141121_add_session_expire_delay_for_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddSessionExpireDelayForApplicationSettings < ActiveRecord::Migration def change unless column_exists?(:application_settings, :session_expire_delay) diff --git a/db/migrate/20150610065936_add_dashboard_to_users.rb b/db/migrate/20150610065936_add_dashboard_to_users.rb index 2628e450722..df38472f893 100644 --- a/db/migrate/20150610065936_add_dashboard_to_users.rb +++ b/db/migrate/20150610065936_add_dashboard_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDashboardToUsers < ActiveRecord::Migration def up add_column :users, :dashboard, :integer, default: 0 diff --git a/db/migrate/20150620233230_add_default_otp_required_for_login_value.rb b/db/migrate/20150620233230_add_default_otp_required_for_login_value.rb index 8eed8678b2f..da0fd457a34 100644 --- a/db/migrate/20150620233230_add_default_otp_required_for_login_value.rb +++ b/db/migrate/20150620233230_add_default_otp_required_for_login_value.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDefaultOtpRequiredForLoginValue < ActiveRecord::Migration def up execute %q{UPDATE users SET otp_required_for_login = FALSE WHERE otp_required_for_login IS NULL} diff --git a/db/migrate/20150713160110_add_project_view_to_users.rb b/db/migrate/20150713160110_add_project_view_to_users.rb index fe3d206df89..0de5a93035c 100644 --- a/db/migrate/20150713160110_add_project_view_to_users.rb +++ b/db/migrate/20150713160110_add_project_view_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddProjectViewToUsers < ActiveRecord::Migration def change add_column :users, :project_view, :integer, default: 0 diff --git a/db/migrate/20150717130904_add_commits_count_to_project.rb b/db/migrate/20150717130904_add_commits_count_to_project.rb index 9b46daa5933..5799e068c69 100644 --- a/db/migrate/20150717130904_add_commits_count_to_project.rb +++ b/db/migrate/20150717130904_add_commits_count_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCommitsCountToProject < ActiveRecord::Migration def change add_column :projects, :commit_count, :integer, default: 0 diff --git a/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb b/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb index 78d45c7f96b..be30e881c74 100644 --- a/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb +++ b/db/migrate/20150730122406_add_updated_by_to_issuables_and_notes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUpdatedByToIssuablesAndNotes < ActiveRecord::Migration def change add_column :notes, :updated_by_id, :integer diff --git a/db/migrate/20150806104937_create_abuse_reports.rb b/db/migrate/20150806104937_create_abuse_reports.rb index e97dc4cf04c..3c749b5d9a9 100644 --- a/db/migrate/20150806104937_create_abuse_reports.rb +++ b/db/migrate/20150806104937_create_abuse_reports.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateAbuseReports < ActiveRecord::Migration def change create_table :abuse_reports do |t| diff --git a/db/migrate/20150812080800_add_settings_import_sources.rb b/db/migrate/20150812080800_add_settings_import_sources.rb index 276d2fdb2b1..07f417fa3e3 100644 --- a/db/migrate/20150812080800_add_settings_import_sources.rb +++ b/db/migrate/20150812080800_add_settings_import_sources.rb @@ -1,3 +1,4 @@ +# rubocop:disable all require 'yaml' class AddSettingsImportSources < ActiveRecord::Migration diff --git a/db/migrate/20150814065925_remove_oauth_tokens_from_users.rb b/db/migrate/20150814065925_remove_oauth_tokens_from_users.rb index de2078a9268..7eaa7eda311 100644 --- a/db/migrate/20150814065925_remove_oauth_tokens_from_users.rb +++ b/db/migrate/20150814065925_remove_oauth_tokens_from_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveOauthTokensFromUsers < ActiveRecord::Migration def change remove_column :users, :github_access_token, :string diff --git a/db/migrate/20150817163600_deduplicate_user_identities.rb b/db/migrate/20150817163600_deduplicate_user_identities.rb index fceffc48018..b0cfad7d20f 100644 --- a/db/migrate/20150817163600_deduplicate_user_identities.rb +++ b/db/migrate/20150817163600_deduplicate_user_identities.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class DeduplicateUserIdentities < ActiveRecord::Migration def change execute 'DROP TABLE IF EXISTS tt_migration_DeduplicateUserIdentities;' diff --git a/db/migrate/20150818213832_add_sent_notifications.rb b/db/migrate/20150818213832_add_sent_notifications.rb index 43e8d6a1a82..fa0c3ce0acf 100644 --- a/db/migrate/20150818213832_add_sent_notifications.rb +++ b/db/migrate/20150818213832_add_sent_notifications.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddSentNotifications < ActiveRecord::Migration def change create_table :sent_notifications do |t| diff --git a/db/migrate/20150824002011_add_enable_ssl_verification.rb b/db/migrate/20150824002011_add_enable_ssl_verification.rb index 093c068fbde..6e992f08834 100644 --- a/db/migrate/20150824002011_add_enable_ssl_verification.rb +++ b/db/migrate/20150824002011_add_enable_ssl_verification.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddEnableSslVerification < ActiveRecord::Migration def change add_column :web_hooks, :enable_ssl_verification, :boolean, default: false diff --git a/db/migrate/20150826001931_add_ci_tables.rb b/db/migrate/20150826001931_add_ci_tables.rb index c4f51363e57..d1f8506d1fe 100644 --- a/db/migrate/20150826001931_add_ci_tables.rb +++ b/db/migrate/20150826001931_add_ci_tables.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCiTables < ActiveRecord::Migration def change create_table "ci_application_settings", force: true do |t| diff --git a/db/migrate/20150902001023_add_template_to_label.rb b/db/migrate/20150902001023_add_template_to_label.rb index bd381a97b69..0f6ae8d6cc3 100644 --- a/db/migrate/20150902001023_add_template_to_label.rb +++ b/db/migrate/20150902001023_add_template_to_label.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTemplateToLabel < ActiveRecord::Migration def change add_column :labels, :template, :boolean, default: false diff --git a/db/migrate/20150914215247_add_ci_tags.rb b/db/migrate/20150914215247_add_ci_tags.rb index df3390e8a82..b647bc9c8a2 100644 --- a/db/migrate/20150914215247_add_ci_tags.rb +++ b/db/migrate/20150914215247_add_ci_tags.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCiTags < ActiveRecord::Migration def change create_table "ci_taggings", force: true do |t| diff --git a/db/migrate/20150915001905_enable_ssl_verification_by_default.rb b/db/migrate/20150915001905_enable_ssl_verification_by_default.rb index 6e924262a13..3f070139418 100644 --- a/db/migrate/20150915001905_enable_ssl_verification_by_default.rb +++ b/db/migrate/20150915001905_enable_ssl_verification_by_default.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class EnableSslVerificationByDefault < ActiveRecord::Migration def change change_column :web_hooks, :enable_ssl_verification, :boolean, default: true diff --git a/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb b/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb index 90ce6c2db3d..ea2ab6e4093 100644 --- a/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb +++ b/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class EnableSslVerificationForWebHooks < ActiveRecord::Migration def up execute("UPDATE web_hooks SET enable_ssl_verification = true") diff --git a/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb b/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb index 37a27f11935..a504f25b1be 100644 --- a/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb +++ b/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddHelpPageTextToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :help_page_text, :text diff --git a/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb b/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb index 78d9e5f61a1..a18ed93cf37 100644 --- a/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb +++ b/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexForCommittedAtAndId < ActiveRecord::Migration def change add_index :ci_commits, [:project_id, :committed_at, :id] diff --git a/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb b/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb index 6cf668a170e..c9b6e035122 100644 --- a/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb +++ b/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCiEnabledToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :ci_enabled, :boolean, null: false, default: true diff --git a/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb b/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb index 0aad6fe5e6e..e1818b566d7 100644 --- a/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb +++ b/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveInvalidMilestonesFromMergeRequests < ActiveRecord::Migration def up execute("UPDATE merge_requests SET milestone_id = NULL where milestone_id NOT IN (SELECT id FROM milestones)") diff --git a/db/migrate/20150920010715_add_consumed_timestep_to_users.rb b/db/migrate/20150920010715_add_consumed_timestep_to_users.rb index c8438b3f6aa..e6975f5b9fe 100644 --- a/db/migrate/20150920010715_add_consumed_timestep_to_users.rb +++ b/db/migrate/20150920010715_add_consumed_timestep_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddConsumedTimestepToUsers < ActiveRecord::Migration def change add_column :users, :consumed_timestep, :integer diff --git a/db/migrate/20150920161119_add_line_code_to_sent_notification.rb b/db/migrate/20150920161119_add_line_code_to_sent_notification.rb index d9af4e71751..1bcb06e4bda 100644 --- a/db/migrate/20150920161119_add_line_code_to_sent_notification.rb +++ b/db/migrate/20150920161119_add_line_code_to_sent_notification.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddLineCodeToSentNotification < ActiveRecord::Migration def change add_column :sent_notifications, :line_code, :string diff --git a/db/migrate/20150924125150_add_project_id_to_ci_commit.rb b/db/migrate/20150924125150_add_project_id_to_ci_commit.rb index 1a761fe0f86..905332b7dc7 100644 --- a/db/migrate/20150924125150_add_project_id_to_ci_commit.rb +++ b/db/migrate/20150924125150_add_project_id_to_ci_commit.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddProjectIdToCiCommit < ActiveRecord::Migration def up add_column :ci_commits, :gl_project_id, :integer diff --git a/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb b/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb index 2be57b6062e..fb0e0ba1fa5 100644 --- a/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb +++ b/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateProjectIdForCiCommits < ActiveRecord::Migration def up subquery = 'SELECT gitlab_id FROM ci_projects WHERE ci_projects.id = ci_commits.project_id' diff --git a/db/migrate/20150930001110_merge_request_error_field.rb b/db/migrate/20150930001110_merge_request_error_field.rb index c2ee498ef3f..71a8ae3938a 100644 --- a/db/migrate/20150930001110_merge_request_error_field.rb +++ b/db/migrate/20150930001110_merge_request_error_field.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MergeRequestErrorField < ActiveRecord::Migration def up add_column :merge_requests, :merge_error, :string diff --git a/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb b/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb index 8d47dac6441..229c9942b50 100644 --- a/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb +++ b/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNullToNameForCiProjects < ActiveRecord::Migration def up change_column_null :ci_projects, :name, true diff --git a/db/migrate/20150930110012_add_group_share_lock.rb b/db/migrate/20150930110012_add_group_share_lock.rb index 78d1a4538f2..96938bf9ab6 100644 --- a/db/migrate/20150930110012_add_group_share_lock.rb +++ b/db/migrate/20150930110012_add_group_share_lock.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddGroupShareLock < ActiveRecord::Migration def change add_column :namespaces, :share_with_group_lock, :boolean, default: false diff --git a/db/migrate/20151002112914_add_stage_idx_to_builds.rb b/db/migrate/20151002112914_add_stage_idx_to_builds.rb index 68a745ffef4..4297ba0e7c8 100644 --- a/db/migrate/20151002112914_add_stage_idx_to_builds.rb +++ b/db/migrate/20151002112914_add_stage_idx_to_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddStageIdxToBuilds < ActiveRecord::Migration def change add_column :ci_builds, :stage_idx, :integer diff --git a/db/migrate/20151002121400_add_index_for_builds.rb b/db/migrate/20151002121400_add_index_for_builds.rb index 4ffc1363910..bd945c54540 100644 --- a/db/migrate/20151002121400_add_index_for_builds.rb +++ b/db/migrate/20151002121400_add_index_for_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexForBuilds < ActiveRecord::Migration def up add_index :ci_builds, [:commit_id, :stage_idx, :created_at] diff --git a/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb b/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb index e3d2ac1cea5..3c0fcf6c45d 100644 --- a/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb +++ b/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRefAndTagToBuilds < ActiveRecord::Migration def change add_column :ci_builds, :tag, :boolean diff --git a/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb b/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb index 01d7b3f6773..52217ce5af2 100644 --- a/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb +++ b/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateRefAndTagToBuild < ActiveRecord::Migration def change execute('UPDATE ci_builds SET ref=(SELECT ref FROM ci_commits WHERE ci_commits.id = ci_builds.commit_id) WHERE ref IS NULL') diff --git a/db/migrate/20151005075649_add_user_id_to_build.rb b/db/migrate/20151005075649_add_user_id_to_build.rb index 0f4b92b8b79..be9d403e002 100644 --- a/db/migrate/20151005075649_add_user_id_to_build.rb +++ b/db/migrate/20151005075649_add_user_id_to_build.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUserIdToBuild < ActiveRecord::Migration def change add_column :ci_builds, :user_id, :integer diff --git a/db/migrate/20151005150751_add_layout_option_for_users.rb b/db/migrate/20151005150751_add_layout_option_for_users.rb index ead9b1f8977..7e68606969f 100644 --- a/db/migrate/20151005150751_add_layout_option_for_users.rb +++ b/db/migrate/20151005150751_add_layout_option_for_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddLayoutOptionForUsers < ActiveRecord::Migration def change add_column :users, :layout, :integer, default: 0 diff --git a/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb b/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb index be6aa810bb5..07dba598749 100644 --- a/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb +++ b/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveCiEnabledFromApplicationSettings < ActiveRecord::Migration def change remove_column :application_settings, :ci_enabled, :boolean, null: false, default: true diff --git a/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb b/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb index 7f6cd6d5a78..38208e59804 100644 --- a/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb +++ b/db/migrate/20151007120511_namespaces_projects_path_lower_indexes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class NamespacesProjectsPathLowerIndexes < ActiveRecord::Migration disable_ddl_transaction! diff --git a/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb b/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb index 2f2dc776785..6080d2a0fcf 100644 --- a/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb +++ b/db/migrate/20151008110232_add_users_lower_username_email_indexes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUsersLowerUsernameEmailIndexes < ActiveRecord::Migration disable_ddl_transaction! diff --git a/db/migrate/20151008123042_add_type_and_description_to_builds.rb b/db/migrate/20151008123042_add_type_and_description_to_builds.rb index c72b1c611c6..a19eb6c6c49 100644 --- a/db/migrate/20151008123042_add_type_and_description_to_builds.rb +++ b/db/migrate/20151008123042_add_type_and_description_to_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTypeAndDescriptionToBuilds < ActiveRecord::Migration def change add_column :ci_builds, :type, :string diff --git a/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb b/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb index f5c44babd84..306fa7092ea 100644 --- a/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb +++ b/db/migrate/20151008130321_migrate_name_to_description_for_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateNameToDescriptionForBuilds < ActiveRecord::Migration def change execute("UPDATE ci_builds SET type='Ci::Build' WHERE type IS NULL") diff --git a/db/migrate/20151008143519_add_admin_notification_email_setting.rb b/db/migrate/20151008143519_add_admin_notification_email_setting.rb index 0bb581efe2c..f48ec9aa4a6 100644 --- a/db/migrate/20151008143519_add_admin_notification_email_setting.rb +++ b/db/migrate/20151008143519_add_admin_notification_email_setting.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAdminNotificationEmailSetting < ActiveRecord::Migration def change add_column :application_settings, :admin_notification_email, :string diff --git a/db/migrate/20151012173029_set_jira_service_api_url.rb b/db/migrate/20151012173029_set_jira_service_api_url.rb index 2af99e0db0b..2b6f61428c0 100644 --- a/db/migrate/20151012173029_set_jira_service_api_url.rb +++ b/db/migrate/20151012173029_set_jira_service_api_url.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class SetJiraServiceApiUrl < ActiveRecord::Migration # This migration can be performed online without errors, but some Jira API calls may be missed # when doing so because api_url is not yet available. diff --git a/db/migrate/20151013092124_add_artifacts_file_to_builds.rb b/db/migrate/20151013092124_add_artifacts_file_to_builds.rb index 5a299f7b26d..a54ac9d57a4 100644 --- a/db/migrate/20151013092124_add_artifacts_file_to_builds.rb +++ b/db/migrate/20151013092124_add_artifacts_file_to_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddArtifactsFileToBuilds < ActiveRecord::Migration def change add_column :ci_builds, :artifacts_file, :text diff --git a/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb b/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb index 52a47aa9c54..eb3351eb767 100644 --- a/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb +++ b/db/migrate/20151016131433_add_ci_projects_gl_project_id_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCiProjectsGlProjectIdIndex < ActiveRecord::Migration def change add_index :ci_commits, :gl_project_id diff --git a/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb b/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb index 7f1af1c7583..899e004d610 100644 --- a/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb +++ b/db/migrate/20151016195451_add_ci_builds_and_projects_indexes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCiBuildsAndProjectsIndexes < ActiveRecord::Migration def change add_index :ci_projects, :gitlab_id diff --git a/db/migrate/20151016195706_add_notes_line_code_index.rb b/db/migrate/20151016195706_add_notes_line_code_index.rb index aeeb1a759fa..3298630c1e8 100644 --- a/db/migrate/20151016195706_add_notes_line_code_index.rb +++ b/db/migrate/20151016195706_add_notes_line_code_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNotesLineCodeIndex < ActiveRecord::Migration def change add_index :notes, :line_code diff --git a/db/migrate/20151019111551_fix_build_tags.rb b/db/migrate/20151019111551_fix_build_tags.rb index 299a24b0a7c..8c05acfc190 100644 --- a/db/migrate/20151019111551_fix_build_tags.rb +++ b/db/migrate/20151019111551_fix_build_tags.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class FixBuildTags < ActiveRecord::Migration def up execute("UPDATE taggings SET taggable_type='CommitStatus' WHERE taggable_type='Ci::Build'") diff --git a/db/migrate/20151019111703_fail_build_without_names.rb b/db/migrate/20151019111703_fail_build_without_names.rb index dcdb5d1b25d..362e31eb435 100644 --- a/db/migrate/20151019111703_fail_build_without_names.rb +++ b/db/migrate/20151019111703_fail_build_without_names.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class FailBuildWithoutNames < ActiveRecord::Migration def up execute("UPDATE ci_builds SET status='failed' WHERE name IS NULL AND status='pending'") diff --git a/db/migrate/20151020145526_add_services_template_index.rb b/db/migrate/20151020145526_add_services_template_index.rb index 1b04f313565..14ff07bd726 100644 --- a/db/migrate/20151020145526_add_services_template_index.rb +++ b/db/migrate/20151020145526_add_services_template_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddServicesTemplateIndex < ActiveRecord::Migration def change add_index :services, :template diff --git a/db/migrate/20151020173516_ci_limits_to_mysql.rb b/db/migrate/20151020173516_ci_limits_to_mysql.rb index 9bb960082f5..5314611cbcd 100644 --- a/db/migrate/20151020173516_ci_limits_to_mysql.rb +++ b/db/migrate/20151020173516_ci_limits_to_mysql.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CiLimitsToMysql < ActiveRecord::Migration def change return unless ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/ diff --git a/db/migrate/20151020173906_add_ci_builds_index_for_status.rb b/db/migrate/20151020173906_add_ci_builds_index_for_status.rb index c3f0e0606da..81a31e46ff8 100644 --- a/db/migrate/20151020173906_add_ci_builds_index_for_status.rb +++ b/db/migrate/20151020173906_add_ci_builds_index_for_status.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCiBuildsIndexForStatus < ActiveRecord::Migration def change add_index :ci_builds, [:commit_id, :status, :type] diff --git a/db/migrate/20151023112551_fail_build_with_empty_name.rb b/db/migrate/20151023112551_fail_build_with_empty_name.rb index 41c0f0649cd..0666dfeaef4 100644 --- a/db/migrate/20151023112551_fail_build_with_empty_name.rb +++ b/db/migrate/20151023112551_fail_build_with_empty_name.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class FailBuildWithEmptyName < ActiveRecord::Migration def up execute("UPDATE ci_builds SET status='failed' WHERE (name IS NULL OR name='') AND status='pending'") diff --git a/db/migrate/20151023144219_remove_satellites.rb b/db/migrate/20151023144219_remove_satellites.rb index e73f300028a..98fe0bd7d1d 100644 --- a/db/migrate/20151023144219_remove_satellites.rb +++ b/db/migrate/20151023144219_remove_satellites.rb @@ -1,3 +1,4 @@ +# rubocop:disable all require 'fileutils' class RemoveSatellites < ActiveRecord::Migration diff --git a/db/migrate/20151026182941_add_project_path_index.rb b/db/migrate/20151026182941_add_project_path_index.rb index a62fe199d70..117f65c1a1b 100644 --- a/db/migrate/20151026182941_add_project_path_index.rb +++ b/db/migrate/20151026182941_add_project_path_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddProjectPathIndex < ActiveRecord::Migration def up add_index :projects, :path diff --git a/db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb b/db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb index ceb52f0c222..4a989669464 100644 --- a/db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb +++ b/db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMergeWhenBuildSucceedsToMergeRequest < ActiveRecord::Migration def change add_column :merge_requests, :merge_params, :text diff --git a/db/migrate/20151103001141_add_public_to_group.rb b/db/migrate/20151103001141_add_public_to_group.rb index 635346300c2..ba1f7c27832 100644 --- a/db/migrate/20151103001141_add_public_to_group.rb +++ b/db/migrate/20151103001141_add_public_to_group.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPublicToGroup < ActiveRecord::Migration def change add_column :namespaces, :public, :boolean, default: false diff --git a/db/migrate/20151103133339_add_shared_runners_setting.rb b/db/migrate/20151103133339_add_shared_runners_setting.rb index 4231dfd5c2e..b5b34d4ca61 100644 --- a/db/migrate/20151103133339_add_shared_runners_setting.rb +++ b/db/migrate/20151103133339_add_shared_runners_setting.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddSharedRunnersSetting < ActiveRecord::Migration def up add_column :application_settings, :shared_runners_enabled, :boolean, default: true, null: false diff --git a/db/migrate/20151103134857_create_lfs_objects.rb b/db/migrate/20151103134857_create_lfs_objects.rb index 2d04c170a88..745b52e2b24 100644 --- a/db/migrate/20151103134857_create_lfs_objects.rb +++ b/db/migrate/20151103134857_create_lfs_objects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateLfsObjects < ActiveRecord::Migration def change create_table :lfs_objects do |t| diff --git a/db/migrate/20151103134958_create_lfs_objects_projects.rb b/db/migrate/20151103134958_create_lfs_objects_projects.rb index f3f58b931ec..3178e85b899 100644 --- a/db/migrate/20151103134958_create_lfs_objects_projects.rb +++ b/db/migrate/20151103134958_create_lfs_objects_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateLfsObjectsProjects < ActiveRecord::Migration def change create_table :lfs_objects_projects do |t| diff --git a/db/migrate/20151104105513_add_file_to_lfs_objects.rb b/db/migrate/20151104105513_add_file_to_lfs_objects.rb index 7c57f3f0df6..4e46ae8101c 100644 --- a/db/migrate/20151104105513_add_file_to_lfs_objects.rb +++ b/db/migrate/20151104105513_add_file_to_lfs_objects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddFileToLfsObjects < ActiveRecord::Migration def change add_column :lfs_objects, :file, :string diff --git a/db/migrate/20151105094515_create_releases.rb b/db/migrate/20151105094515_create_releases.rb index fe4608c6662..145b8db1486 100644 --- a/db/migrate/20151105094515_create_releases.rb +++ b/db/migrate/20151105094515_create_releases.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateReleases < ActiveRecord::Migration def change create_table :releases do |t| diff --git a/db/migrate/20151106000015_add_is_award_to_notes.rb b/db/migrate/20151106000015_add_is_award_to_notes.rb index 02b271637e9..b463d939b78 100644 --- a/db/migrate/20151106000015_add_is_award_to_notes.rb +++ b/db/migrate/20151106000015_add_is_award_to_notes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIsAwardToNotes < ActiveRecord::Migration def change add_column :notes, :is_award, :boolean, default: false, null: false diff --git a/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb b/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb index 01d8c0f043e..25106ace7e9 100644 --- a/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb +++ b/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMaxArtifactsSizeToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :max_artifacts_size, :integer, default: 100, null: false diff --git a/db/migrate/20151109134526_add_issues_state_index.rb b/db/migrate/20151109134526_add_issues_state_index.rb index 1c4d2e30171..7a9970e8591 100644 --- a/db/migrate/20151109134526_add_issues_state_index.rb +++ b/db/migrate/20151109134526_add_issues_state_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIssuesStateIndex < ActiveRecord::Migration def change add_index :issues, :state diff --git a/db/migrate/20151109134916_add_projects_visibility_level_index.rb b/db/migrate/20151109134916_add_projects_visibility_level_index.rb index 600b4bafd98..471db437b11 100644 --- a/db/migrate/20151109134916_add_projects_visibility_level_index.rb +++ b/db/migrate/20151109134916_add_projects_visibility_level_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddProjectsVisibilityLevelIndex < ActiveRecord::Migration def change add_index :projects, :visibility_level diff --git a/db/migrate/20151110125604_add_import_error_to_project.rb b/db/migrate/20151110125604_add_import_error_to_project.rb index 7fc990f8d0a..793358c305e 100644 --- a/db/migrate/20151110125604_add_import_error_to_project.rb +++ b/db/migrate/20151110125604_add_import_error_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddImportErrorToProject < ActiveRecord::Migration def change add_column :projects, :import_error, :text diff --git a/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb b/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb index d10f1f6e605..00a4c74ffbc 100644 --- a/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb +++ b/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexForLfsOidAndSize < ActiveRecord::Migration def change add_index :lfs_objects, :oid diff --git a/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb b/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb index 41b93da0a86..1f192544ea1 100644 --- a/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb +++ b/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUniqueForLfsOidIndex < ActiveRecord::Migration def change remove_index :lfs_objects, :oid diff --git a/db/migrate/20151118162244_add_projects_public_index.rb b/db/migrate/20151118162244_add_projects_public_index.rb index fded70e3c0c..589f124c21e 100644 --- a/db/migrate/20151118162244_add_projects_public_index.rb +++ b/db/migrate/20151118162244_add_projects_public_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddProjectsPublicIndex < ActiveRecord::Migration def change add_index :namespaces, :public diff --git a/db/migrate/20151201203948_raise_hook_url_limit.rb b/db/migrate/20151201203948_raise_hook_url_limit.rb index 98a7fca6f6f..c490b7ace0f 100644 --- a/db/migrate/20151201203948_raise_hook_url_limit.rb +++ b/db/migrate/20151201203948_raise_hook_url_limit.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RaiseHookUrlLimit < ActiveRecord::Migration def change change_column :web_hooks, :url, :string, limit: 2000 diff --git a/db/migrate/20151203162133_add_hide_project_limit_to_users.rb b/db/migrate/20151203162133_add_hide_project_limit_to_users.rb index 6ffadfa1894..5dc6d8bf445 100644 --- a/db/migrate/20151203162133_add_hide_project_limit_to_users.rb +++ b/db/migrate/20151203162133_add_hide_project_limit_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddHideProjectLimitToUsers < ActiveRecord::Migration def change add_column :users, :hide_project_limit, :boolean, default: false diff --git a/db/migrate/20151203162134_add_build_events_to_services.rb b/db/migrate/20151203162134_add_build_events_to_services.rb index c5542cb864d..455882e5ec0 100644 --- a/db/migrate/20151203162134_add_build_events_to_services.rb +++ b/db/migrate/20151203162134_add_build_events_to_services.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddBuildEventsToServices < ActiveRecord::Migration def change add_column :services, :build_events, :boolean, default: false, null: false diff --git a/db/migrate/20151209144329_migrate_ci_web_hooks.rb b/db/migrate/20151209144329_migrate_ci_web_hooks.rb index d7e196e6763..cb1e556623a 100644 --- a/db/migrate/20151209144329_migrate_ci_web_hooks.rb +++ b/db/migrate/20151209144329_migrate_ci_web_hooks.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateCiWebHooks < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20151209145909_migrate_ci_emails.rb b/db/migrate/20151209145909_migrate_ci_emails.rb index 7f330a2cf0a..6b7a106814d 100644 --- a/db/migrate/20151209145909_migrate_ci_emails.rb +++ b/db/migrate/20151209145909_migrate_ci_emails.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateCiEmails < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20151210030143_add_unlock_token_to_user.rb b/db/migrate/20151210030143_add_unlock_token_to_user.rb index 0ea66ba65df..d23c648f782 100644 --- a/db/migrate/20151210030143_add_unlock_token_to_user.rb +++ b/db/migrate/20151210030143_add_unlock_token_to_user.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddUnlockTokenToUser < ActiveRecord::Migration def change add_column :users, :unlock_token, :string diff --git a/db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb b/db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb index 00f88180e46..92c7b5befd2 100644 --- a/db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb +++ b/db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRunnersRegistrationTokenToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :runners_registration_token, :string diff --git a/db/migrate/20151210125232_migrate_ci_slack_service.rb b/db/migrate/20151210125232_migrate_ci_slack_service.rb index f14efa3e95d..633d5148d97 100644 --- a/db/migrate/20151210125232_migrate_ci_slack_service.rb +++ b/db/migrate/20151210125232_migrate_ci_slack_service.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateCiSlackService < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb b/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb index b9e04323576..dae084ce180 100644 --- a/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb +++ b/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateCiHipChatService < ActiveRecord::Migration include Gitlab::Database diff --git a/db/migrate/20151210125928_add_ci_to_project.rb b/db/migrate/20151210125928_add_ci_to_project.rb index 8c167f64a2b..a9ff49a3f7e 100644 --- a/db/migrate/20151210125928_add_ci_to_project.rb +++ b/db/migrate/20151210125928_add_ci_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCiToProject < ActiveRecord::Migration def change add_column :projects, :ci_id, :integer diff --git a/db/migrate/20151210125929_add_project_id_to_ci.rb b/db/migrate/20151210125929_add_project_id_to_ci.rb index 84273591fa2..b5de64b82ca 100644 --- a/db/migrate/20151210125929_add_project_id_to_ci.rb +++ b/db/migrate/20151210125929_add_project_id_to_ci.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddProjectIdToCi < ActiveRecord::Migration def change add_column :ci_builds, :gl_project_id, :integer diff --git a/db/migrate/20151210125930_migrate_ci_to_project.rb b/db/migrate/20151210125930_migrate_ci_to_project.rb index c32c7feb193..bb6d74ae212 100644 --- a/db/migrate/20151210125930_migrate_ci_to_project.rb +++ b/db/migrate/20151210125930_migrate_ci_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class MigrateCiToProject < ActiveRecord::Migration def up migrate_project_id_for_table('ci_runner_projects') diff --git a/db/migrate/20151210125931_add_index_to_ci_tables.rb b/db/migrate/20151210125931_add_index_to_ci_tables.rb index 5e129c9303d..d87d335cf6b 100644 --- a/db/migrate/20151210125931_add_index_to_ci_tables.rb +++ b/db/migrate/20151210125931_add_index_to_ci_tables.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexToCiTables < ActiveRecord::Migration def change add_index :ci_builds, :gl_project_id diff --git a/db/migrate/20151210125932_drop_null_for_ci_tables.rb b/db/migrate/20151210125932_drop_null_for_ci_tables.rb index c520c2ed56f..e1a0a964589 100644 --- a/db/migrate/20151210125932_drop_null_for_ci_tables.rb +++ b/db/migrate/20151210125932_drop_null_for_ci_tables.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class DropNullForCiTables < ActiveRecord::Migration def change remove_index :ci_variables, :project_id diff --git a/db/migrate/20151218154042_add_tfa_to_application_settings.rb b/db/migrate/20151218154042_add_tfa_to_application_settings.rb index dd95db775c5..afdaf76b917 100644 --- a/db/migrate/20151218154042_add_tfa_to_application_settings.rb +++ b/db/migrate/20151218154042_add_tfa_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTfaToApplicationSettings < ActiveRecord::Migration def change change_table :application_settings do |t| diff --git a/db/migrate/20151221234414_add_tfa_additional_fields.rb b/db/migrate/20151221234414_add_tfa_additional_fields.rb index c16df47932f..c3e4aaa606a 100644 --- a/db/migrate/20151221234414_add_tfa_additional_fields.rb +++ b/db/migrate/20151221234414_add_tfa_additional_fields.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTfaAdditionalFields < ActiveRecord::Migration def change change_table :users do |t| diff --git a/db/migrate/20151224123230_rename_emojis.rb b/db/migrate/20151224123230_rename_emojis.rb index 62d921dfdcc..2c24f3beeea 100644 --- a/db/migrate/20151224123230_rename_emojis.rb +++ b/db/migrate/20151224123230_rename_emojis.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # Migration type: online without errors (works on previous version and new one) class RenameEmojis < ActiveRecord::Migration def up diff --git a/db/migrate/20151228111122_remove_public_from_namespace.rb b/db/migrate/20151228111122_remove_public_from_namespace.rb index f4c848bbf47..bcb322d9cba 100644 --- a/db/migrate/20151228111122_remove_public_from_namespace.rb +++ b/db/migrate/20151228111122_remove_public_from_namespace.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # Migration type: online class RemovePublicFromNamespace < ActiveRecord::Migration def change diff --git a/db/migrate/20151228150906_influxdb_settings.rb b/db/migrate/20151228150906_influxdb_settings.rb index 3012bd52cfd..2e080a02e6a 100644 --- a/db/migrate/20151228150906_influxdb_settings.rb +++ b/db/migrate/20151228150906_influxdb_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class InfluxdbSettings < ActiveRecord::Migration def change add_column :application_settings, :metrics_enabled, :boolean, default: false diff --git a/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb b/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb index 259fd0248d2..e0dd19b2b06 100644 --- a/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb +++ b/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRecaptchaToApplicationSettings < ActiveRecord::Migration def change change_table :application_settings do |t| diff --git a/db/migrate/20151229102248_influxdb_udp_port_setting.rb b/db/migrate/20151229102248_influxdb_udp_port_setting.rb index ae0499f936d..3e1bfd43899 100644 --- a/db/migrate/20151229102248_influxdb_udp_port_setting.rb +++ b/db/migrate/20151229102248_influxdb_udp_port_setting.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class InfluxdbUdpPortSetting < ActiveRecord::Migration def change add_column :application_settings, :metrics_port, :integer, default: 8089 diff --git a/db/migrate/20151229112614_influxdb_remote_database_setting.rb b/db/migrate/20151229112614_influxdb_remote_database_setting.rb index f0e1ee1e7a7..d2ac906ead3 100644 --- a/db/migrate/20151229112614_influxdb_remote_database_setting.rb +++ b/db/migrate/20151229112614_influxdb_remote_database_setting.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class InfluxdbRemoteDatabaseSetting < ActiveRecord::Migration def change remove_column :application_settings, :metrics_database diff --git a/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb b/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb index 6c282fc5039..4fcca06d905 100644 --- a/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb +++ b/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddArtifactsMetadataToCiBuild < ActiveRecord::Migration def change add_column :ci_builds, :artifacts_metadata, :text diff --git a/db/migrate/20151231152326_add_akismet_to_application_settings.rb b/db/migrate/20151231152326_add_akismet_to_application_settings.rb index 3f52c758f9a..7b0fab6f557 100644 --- a/db/migrate/20151231152326_add_akismet_to_application_settings.rb +++ b/db/migrate/20151231152326_add_akismet_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAkismetToApplicationSettings < ActiveRecord::Migration def change change_table :application_settings do |t| diff --git a/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb b/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb index 78fdfeaf5cf..0bdd639eb21 100644 --- a/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb +++ b/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveAlertTypeFromBroadcastMessages < ActiveRecord::Migration def change remove_column :broadcast_messages, :alert_type, :integer diff --git a/db/migrate/20160106162223_add_index_milestones_title.rb b/db/migrate/20160106162223_add_index_milestones_title.rb index 767885e2aac..9b9b6445a08 100644 --- a/db/migrate/20160106162223_add_index_milestones_title.rb +++ b/db/migrate/20160106162223_add_index_milestones_title.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexMilestonesTitle < ActiveRecord::Migration def change add_index :milestones, :title diff --git a/db/migrate/20160106164438_remove_influxdb_credentials.rb b/db/migrate/20160106164438_remove_influxdb_credentials.rb index 47e74400b97..987d75d6fda 100644 --- a/db/migrate/20160106164438_remove_influxdb_credentials.rb +++ b/db/migrate/20160106164438_remove_influxdb_credentials.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveInfluxdbCredentials < ActiveRecord::Migration def change remove_column :application_settings, :metrics_username, :string diff --git a/db/migrate/20160109054846_create_spam_logs.rb b/db/migrate/20160109054846_create_spam_logs.rb index f12fe9f8f78..f7103276639 100644 --- a/db/migrate/20160109054846_create_spam_logs.rb +++ b/db/migrate/20160109054846_create_spam_logs.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateSpamLogs < ActiveRecord::Migration def change create_table :spam_logs do |t| diff --git a/db/migrate/20160113111034_add_metrics_sample_interval.rb b/db/migrate/20160113111034_add_metrics_sample_interval.rb index b741f5d2c75..c1041da818c 100644 --- a/db/migrate/20160113111034_add_metrics_sample_interval.rb +++ b/db/migrate/20160113111034_add_metrics_sample_interval.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMetricsSampleInterval < ActiveRecord::Migration def change add_column :application_settings, :metrics_sample_interval, :integer, diff --git a/db/migrate/20160118155830_add_sentry_to_application_settings.rb b/db/migrate/20160118155830_add_sentry_to_application_settings.rb index fa7ff9d9228..a6f715263ef 100644 --- a/db/migrate/20160118155830_add_sentry_to_application_settings.rb +++ b/db/migrate/20160118155830_add_sentry_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddSentryToApplicationSettings < ActiveRecord::Migration def change change_table :application_settings do |t| diff --git a/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb b/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb index 26606b10b54..19ea40b5547 100644 --- a/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb +++ b/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIpBlockingSettingsToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :ip_blocking_enabled, :boolean, default: false diff --git a/db/migrate/20160119111158_add_services_category.rb b/db/migrate/20160119111158_add_services_category.rb index a9110a8418b..f77484b2f96 100644 --- a/db/migrate/20160119111158_add_services_category.rb +++ b/db/migrate/20160119111158_add_services_category.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddServicesCategory < ActiveRecord::Migration def up add_column :services, :category, :string, default: 'common', null: false diff --git a/db/migrate/20160119112418_add_services_default.rb b/db/migrate/20160119112418_add_services_default.rb index 69a42d7b873..7fa531899fe 100644 --- a/db/migrate/20160119112418_add_services_default.rb +++ b/db/migrate/20160119112418_add_services_default.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddServicesDefault < ActiveRecord::Migration def up add_column :services, :default, :boolean, default: false diff --git a/db/migrate/20160119145451_add_ldap_email_to_users.rb b/db/migrate/20160119145451_add_ldap_email_to_users.rb index 654d31ab15a..5b2b0bd31ca 100644 --- a/db/migrate/20160119145451_add_ldap_email_to_users.rb +++ b/db/migrate/20160119145451_add_ldap_email_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddLdapEmailToUsers < ActiveRecord::Migration def up add_column :users, :ldap_email, :boolean, default: false, null: false diff --git a/db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb b/db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb index d6c6aa4a4e8..3837208f81e 100644 --- a/db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb +++ b/db/migrate/20160120172143_add_base_commit_sha_to_merge_request_diffs.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddBaseCommitShaToMergeRequestDiffs < ActiveRecord::Migration def change add_column :merge_request_diffs, :base_commit_sha, :string diff --git a/db/migrate/20160121030729_add_email_author_in_body_to_application_settings.rb b/db/migrate/20160121030729_add_email_author_in_body_to_application_settings.rb index d50791410f9..9a2570ae544 100644 --- a/db/migrate/20160121030729_add_email_author_in_body_to_application_settings.rb +++ b/db/migrate/20160121030729_add_email_author_in_body_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddEmailAuthorInBodyToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :email_author_in_body, :boolean, default: false diff --git a/db/migrate/20160122185421_add_pending_delete_to_project.rb b/db/migrate/20160122185421_add_pending_delete_to_project.rb index 046a5d8fc32..61db852843f 100644 --- a/db/migrate/20160122185421_add_pending_delete_to_project.rb +++ b/db/migrate/20160122185421_add_pending_delete_to_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddPendingDeleteToProject < ActiveRecord::Migration def change add_column :projects, :pending_delete, :boolean, default: false diff --git a/db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb b/db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb index 41821cdcc42..60ecda998dd 100644 --- a/db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb +++ b/db/migrate/20160128212447_remove_ip_blocking_settings_from_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveIpBlockingSettingsFromApplicationSettings < ActiveRecord::Migration def change remove_column :application_settings, :ip_blocking_enabled, :boolean, default: false diff --git a/db/migrate/20160128233227_change_lfs_objects_size_column.rb b/db/migrate/20160128233227_change_lfs_objects_size_column.rb index e7fd1f71777..645c0cdb192 100644 --- a/db/migrate/20160128233227_change_lfs_objects_size_column.rb +++ b/db/migrate/20160128233227_change_lfs_objects_size_column.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ChangeLfsObjectsSizeColumn < ActiveRecord::Migration def change change_column :lfs_objects, :size, :integer, limit: 8 diff --git a/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb b/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb index d3ea956952e..b10c0602e24 100644 --- a/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb +++ b/db/migrate/20160129135155_remove_dot_atom_path_ending_of_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveDotAtomPathEndingOfProjects < ActiveRecord::Migration include Gitlab::ShellAdapter diff --git a/db/migrate/20160129155512_add_merge_commit_sha_to_merge_requests.rb b/db/migrate/20160129155512_add_merge_commit_sha_to_merge_requests.rb index f0d94226514..332b5a756e8 100644 --- a/db/migrate/20160129155512_add_merge_commit_sha_to_merge_requests.rb +++ b/db/migrate/20160129155512_add_merge_commit_sha_to_merge_requests.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMergeCommitShaToMergeRequests < ActiveRecord::Migration def change add_column :merge_requests, :merge_commit_sha, :string diff --git a/db/migrate/20160202091601_add_erasable_to_ci_build.rb b/db/migrate/20160202091601_add_erasable_to_ci_build.rb index f9912f2274e..767ae160d08 100644 --- a/db/migrate/20160202091601_add_erasable_to_ci_build.rb +++ b/db/migrate/20160202091601_add_erasable_to_ci_build.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddErasableToCiBuild < ActiveRecord::Migration def change add_reference :ci_builds, :erased_by, references: :users, index: true diff --git a/db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb b/db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb index 793984343b4..2c5cb307fad 100644 --- a/db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb +++ b/db/migrate/20160202164642_add_allow_guest_to_access_builds_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddAllowGuestToAccessBuildsProject < ActiveRecord::Migration def change add_column :projects, :public_builds, :boolean, default: true, null: false diff --git a/db/migrate/20160204144558_add_real_size_to_merge_request_diffs.rb b/db/migrate/20160204144558_add_real_size_to_merge_request_diffs.rb index f996ae74dca..11b6ff31000 100644 --- a/db/migrate/20160204144558_add_real_size_to_merge_request_diffs.rb +++ b/db/migrate/20160204144558_add_real_size_to_merge_request_diffs.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRealSizeToMergeRequestDiffs < ActiveRecord::Migration def change add_column :merge_request_diffs, :real_size, :string diff --git a/db/migrate/20160209130428_add_index_to_snippet.rb b/db/migrate/20160209130428_add_index_to_snippet.rb index 95d5719be59..4d17c3a2917 100644 --- a/db/migrate/20160209130428_add_index_to_snippet.rb +++ b/db/migrate/20160209130428_add_index_to_snippet.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexToSnippet < ActiveRecord::Migration def change add_index :snippets, :updated_at diff --git a/db/migrate/20160212123307_create_tasks.rb b/db/migrate/20160212123307_create_tasks.rb index c3f6f3abc26..20573b01351 100644 --- a/db/migrate/20160212123307_create_tasks.rb +++ b/db/migrate/20160212123307_create_tasks.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateTasks < ActiveRecord::Migration def change create_table :tasks do |t| diff --git a/db/migrate/20160217100506_add_description_to_label.rb b/db/migrate/20160217100506_add_description_to_label.rb index eed6d1f236a..af5af167470 100644 --- a/db/migrate/20160217100506_add_description_to_label.rb +++ b/db/migrate/20160217100506_add_description_to_label.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDescriptionToLabel < ActiveRecord::Migration def change add_column :labels, :description, :string diff --git a/db/migrate/20160217174422_add_note_to_tasks.rb b/db/migrate/20160217174422_add_note_to_tasks.rb index da5cb2e05db..a9a2b77e423 100644 --- a/db/migrate/20160217174422_add_note_to_tasks.rb +++ b/db/migrate/20160217174422_add_note_to_tasks.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNoteToTasks < ActiveRecord::Migration def change add_reference :tasks, :note, index: true diff --git a/db/migrate/20160220123949_rename_tasks_to_todos.rb b/db/migrate/20160220123949_rename_tasks_to_todos.rb index 30c10d27146..f16b37537f3 100644 --- a/db/migrate/20160220123949_rename_tasks_to_todos.rb +++ b/db/migrate/20160220123949_rename_tasks_to_todos.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RenameTasksToTodos < ActiveRecord::Migration def change rename_table :tasks, :todos diff --git a/db/migrate/20160222153918_create_appearances_ce.rb b/db/migrate/20160222153918_create_appearances_ce.rb index bec66bcc71e..b2d5949b23f 100644 --- a/db/migrate/20160222153918_create_appearances_ce.rb +++ b/db/migrate/20160222153918_create_appearances_ce.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateAppearancesCe < ActiveRecord::Migration def change unless table_exists?(:appearances) diff --git a/db/migrate/20160223192159_add_confidential_to_issues.rb b/db/migrate/20160223192159_add_confidential_to_issues.rb index e9d47fd589a..5b99ce30e9f 100644 --- a/db/migrate/20160223192159_add_confidential_to_issues.rb +++ b/db/migrate/20160223192159_add_confidential_to_issues.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddConfidentialToIssues < ActiveRecord::Migration def change add_column :issues, :confidential, :boolean, default: false diff --git a/db/migrate/20160225090018_add_delete_at_to_issues.rb b/db/migrate/20160225090018_add_delete_at_to_issues.rb index 3ddbef92978..139f911e1c9 100644 --- a/db/migrate/20160225090018_add_delete_at_to_issues.rb +++ b/db/migrate/20160225090018_add_delete_at_to_issues.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDeleteAtToIssues < ActiveRecord::Migration def change add_column :issues, :deleted_at, :datetime diff --git a/db/migrate/20160225101956_add_delete_at_to_merge_requests.rb b/db/migrate/20160225101956_add_delete_at_to_merge_requests.rb index 9d09105f17d..4ca3f0dcdc5 100644 --- a/db/migrate/20160225101956_add_delete_at_to_merge_requests.rb +++ b/db/migrate/20160225101956_add_delete_at_to_merge_requests.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDeleteAtToMergeRequests < ActiveRecord::Migration def change add_column :merge_requests, :deleted_at, :datetime diff --git a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb index d7b00e3d6ed..375e389e07a 100644 --- a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb +++ b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTrigramIndexesForSearching < ActiveRecord::Migration disable_ddl_transaction! diff --git a/db/migrate/20160227120001_add_event_field_for_web_hook.rb b/db/migrate/20160227120001_add_event_field_for_web_hook.rb index 65f2a47bb3c..89910893ee1 100644 --- a/db/migrate/20160227120001_add_event_field_for_web_hook.rb +++ b/db/migrate/20160227120001_add_event_field_for_web_hook.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddEventFieldForWebHook < ActiveRecord::Migration def change add_column :web_hooks, :wiki_page_events, :boolean, default: false, null: false diff --git a/db/migrate/20160227120047_add_event_to_services.rb b/db/migrate/20160227120047_add_event_to_services.rb index f5040d770de..fe7c54ca4eb 100644 --- a/db/migrate/20160227120047_add_event_to_services.rb +++ b/db/migrate/20160227120047_add_event_to_services.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddEventToServices < ActiveRecord::Migration def change add_column :services, :wiki_page_events, :boolean, default: true diff --git a/db/migrate/20160229193553_add_main_language_to_repository.rb b/db/migrate/20160229193553_add_main_language_to_repository.rb index b5446c6a447..ad5167b4c93 100644 --- a/db/migrate/20160229193553_add_main_language_to_repository.rb +++ b/db/migrate/20160229193553_add_main_language_to_repository.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMainLanguageToRepository < ActiveRecord::Migration def change add_column :projects, :main_language, :string diff --git a/db/migrate/20160301124843_add_visibility_level_to_groups.rb b/db/migrate/20160301124843_add_visibility_level_to_groups.rb index d1b921bb208..a874e6758dd 100644 --- a/db/migrate/20160301124843_add_visibility_level_to_groups.rb +++ b/db/migrate/20160301124843_add_visibility_level_to_groups.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddVisibilityLevelToGroups < ActiveRecord::Migration def up add_column :namespaces, :visibility_level, :integer, null: false, default: Gitlab::VisibilityLevel::PUBLIC diff --git a/db/migrate/20160302151724_add_import_credentials_to_project_import_data.rb b/db/migrate/20160302151724_add_import_credentials_to_project_import_data.rb index ffcd64266e3..1f400566f9f 100644 --- a/db/migrate/20160302151724_add_import_credentials_to_project_import_data.rb +++ b/db/migrate/20160302151724_add_import_credentials_to_project_import_data.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddImportCredentialsToProjectImportData < ActiveRecord::Migration def change add_column :project_import_data, :encrypted_credentials, :text diff --git a/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb b/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb index 8a351cf27a3..ac7eac0ea7c 100644 --- a/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb +++ b/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # Loops through old importer projects that kept a token/password in the import URL # and encrypts the credentials into a separate field in project#import_data # #down method not supported @@ -24,11 +25,11 @@ class RemoveWrongImportUrlFromProjects < ActiveRecord::Migration def process_projects_with_wrong_url projects_with_wrong_import_url.each do |project| begin - import_url = Gitlab::ImportUrl.new(project["import_url"]) + import_url = Gitlab::UrlSanitizer.new(project["import_url"]) update_import_url(import_url, project) update_import_data(import_url, project) - rescue URI::InvalidURIError + rescue Addressable::URI::InvalidURIError nullify_import_url(project) end end diff --git a/db/migrate/20160305220806_remove_expires_at_from_snippets.rb b/db/migrate/20160305220806_remove_expires_at_from_snippets.rb index fc12b5b09e6..cac78703bc2 100644 --- a/db/migrate/20160305220806_remove_expires_at_from_snippets.rb +++ b/db/migrate/20160305220806_remove_expires_at_from_snippets.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveExpiresAtFromSnippets < ActiveRecord::Migration def change remove_column :snippets, :expires_at, :datetime diff --git a/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb b/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb index 49e787d9a9a..10f2b8cc56a 100644 --- a/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb +++ b/db/migrate/20160307221555_disallow_blank_line_code_on_note.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class DisallowBlankLineCodeOnNote < ActiveRecord::Migration def up execute("UPDATE notes SET line_code = NULL WHERE line_code = ''") diff --git a/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb b/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb index 75de5f70fa2..92c0a1e088e 100644 --- a/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb +++ b/db/migrate/20160308212903_add_default_group_visibility_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # Create visibility level field on DB # Sets default_visibility_level to value on settings if not restricted # If value is restricted takes higher visibility level allowed @@ -7,7 +8,9 @@ class AddDefaultGroupVisibilityToApplicationSettings < ActiveRecord::Migration add_column :application_settings, :default_group_visibility, :integer # Unfortunately, this can't be a `default`, since we don't want the configuration specific # `allowed_visibility_level` to end up in schema.rb - execute("UPDATE application_settings SET default_group_visibility = #{allowed_visibility_level}") + + visibility_level = allowed_visibility_level || Gitlab::VisibilityLevel::PRIVATE + execute("UPDATE application_settings SET default_group_visibility = #{visibility_level}") end def down diff --git a/db/migrate/20160309140734_fix_todos.rb b/db/migrate/20160309140734_fix_todos.rb index ebe0fc82305..94fe1e4fdc3 100644 --- a/db/migrate/20160309140734_fix_todos.rb +++ b/db/migrate/20160309140734_fix_todos.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class FixTodos < ActiveRecord::Migration def up execute <<-SQL diff --git a/db/migrate/20160310124959_add_due_date_to_issues.rb b/db/migrate/20160310124959_add_due_date_to_issues.rb index ec08bd9fdfa..a4eb6aaee63 100644 --- a/db/migrate/20160310124959_add_due_date_to_issues.rb +++ b/db/migrate/20160310124959_add_due_date_to_issues.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddDueDateToIssues < ActiveRecord::Migration def change add_column :issues, :due_date, :date diff --git a/db/migrate/20160310185910_add_external_flag_to_users.rb b/db/migrate/20160310185910_add_external_flag_to_users.rb index 54937f1eb71..209496dc786 100644 --- a/db/migrate/20160310185910_add_external_flag_to_users.rb +++ b/db/migrate/20160310185910_add_external_flag_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddExternalFlagToUsers < ActiveRecord::Migration def change add_column :users, :external, :boolean, default: false diff --git a/db/migrate/20160314094147_add_priority_to_label.rb b/db/migrate/20160314094147_add_priority_to_label.rb new file mode 100644 index 00000000000..7fb23cba4c9 --- /dev/null +++ b/db/migrate/20160314094147_add_priority_to_label.rb @@ -0,0 +1,7 @@ +# rubocop:disable all +class AddPriorityToLabel < ActiveRecord::Migration + def change + add_column :labels, :priority, :integer + add_index :labels, :priority + end +end diff --git a/db/migrate/20160314143402_projects_add_pushes_since_gc.rb b/db/migrate/20160314143402_projects_add_pushes_since_gc.rb index 5d30a38bc99..9f8ffe073a3 100644 --- a/db/migrate/20160314143402_projects_add_pushes_since_gc.rb +++ b/db/migrate/20160314143402_projects_add_pushes_since_gc.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ProjectsAddPushesSinceGc < ActiveRecord::Migration def change add_column :projects, :pushes_since_gc, :integer, default: 0 diff --git a/db/migrate/20160315135439_project_add_repository_check.rb b/db/migrate/20160315135439_project_add_repository_check.rb index 8687d5d6296..8fe649246c7 100644 --- a/db/migrate/20160315135439_project_add_repository_check.rb +++ b/db/migrate/20160315135439_project_add_repository_check.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ProjectAddRepositoryCheck < ActiveRecord::Migration def change add_column :projects, :last_repository_check_failed, :boolean diff --git a/db/migrate/20160316123110_ci_runners_token_index.rb b/db/migrate/20160316123110_ci_runners_token_index.rb index 67bf5b4f978..ff3d36d68ee 100644 --- a/db/migrate/20160316123110_ci_runners_token_index.rb +++ b/db/migrate/20160316123110_ci_runners_token_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CiRunnersTokenIndex < ActiveRecord::Migration disable_ddl_transaction! diff --git a/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb b/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb index 6871b3920df..65e0e61c78f 100644 --- a/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb +++ b/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class ChangeTargetIdToNullOnTodos < ActiveRecord::Migration def change change_column_null :todos, :target_id, true diff --git a/db/migrate/20160316204731_add_commit_id_to_todos.rb b/db/migrate/20160316204731_add_commit_id_to_todos.rb index ae19fdd1abd..d79858fc920 100644 --- a/db/migrate/20160316204731_add_commit_id_to_todos.rb +++ b/db/migrate/20160316204731_add_commit_id_to_todos.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCommitIdToTodos < ActiveRecord::Migration def change add_column :todos, :commit_id, :string diff --git a/db/migrate/20160317092222_add_moved_to_to_issue.rb b/db/migrate/20160317092222_add_moved_to_to_issue.rb index 461e7fb3a9b..9dde668ddff 100644 --- a/db/migrate/20160317092222_add_moved_to_to_issue.rb +++ b/db/migrate/20160317092222_add_moved_to_to_issue.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMovedToToIssue < ActiveRecord::Migration def change add_reference :issues, :moved_to, references: :issues diff --git a/db/migrate/20160320204112_index_namespaces_on_visibility_level.rb b/db/migrate/20160320204112_index_namespaces_on_visibility_level.rb index 370b339d45c..07ae7c95477 100644 --- a/db/migrate/20160320204112_index_namespaces_on_visibility_level.rb +++ b/db/migrate/20160320204112_index_namespaces_on_visibility_level.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class IndexNamespacesOnVisibilityLevel < ActiveRecord::Migration def change unless index_exists?(:namespaces, :visibility_level) diff --git a/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb b/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb index 1fff9759d1e..a9a851cfe63 100644 --- a/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb +++ b/db/migrate/20160324020319_remove_todos_for_deleted_issues.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveTodosForDeletedIssues < ActiveRecord::Migration def up execute <<-SQL diff --git a/db/migrate/20160328112808_create_notification_settings.rb b/db/migrate/20160328112808_create_notification_settings.rb index 4755da8b806..7d77e8004ba 100644 --- a/db/migrate/20160328112808_create_notification_settings.rb +++ b/db/migrate/20160328112808_create_notification_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class CreateNotificationSettings < ActiveRecord::Migration def change create_table :notification_settings do |t| diff --git a/db/migrate/20160328115649_migrate_new_notification_setting.rb b/db/migrate/20160328115649_migrate_new_notification_setting.rb index 3c81b2c37bf..eb6b7d07219 100644 --- a/db/migrate/20160328115649_migrate_new_notification_setting.rb +++ b/db/migrate/20160328115649_migrate_new_notification_setting.rb @@ -1,3 +1,4 @@ +# rubocop:disable all # This migration will create one row of NotificationSetting for each Member row # It can take long time on big instances. # diff --git a/db/migrate/20160328121138_add_notification_setting_index.rb b/db/migrate/20160328121138_add_notification_setting_index.rb index 8aebce0244d..667270d6b04 100644 --- a/db/migrate/20160328121138_add_notification_setting_index.rb +++ b/db/migrate/20160328121138_add_notification_setting_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddNotificationSettingIndex < ActiveRecord::Migration def change add_index :notification_settings, :user_id diff --git a/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb b/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb index 275554e736e..a3df8fb4e2e 100644 --- a/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb +++ b/db/migrate/20160329144452_add_index_on_pending_delete_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddIndexOnPendingDeleteProjects < ActiveRecord::Migration def change add_index :projects, :pending_delete diff --git a/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb b/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb index 54cea964ff2..b15af79b9b5 100644 --- a/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb +++ b/db/migrate/20160331133914_remove_todos_for_deleted_merge_requests.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveTodosForDeletedMergeRequests < ActiveRecord::Migration def up execute <<-SQL diff --git a/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb index 0d736e323b6..dec80497fb3 100644 --- a/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb +++ b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveTwitterSharingEnabledFromApplicationSettings < ActiveRecord::Migration def change remove_column :application_settings, :twitter_sharing_enabled, :boolean diff --git a/db/migrate/20160407120251_add_images_enabled_for_project.rb b/db/migrate/20160407120251_add_images_enabled_for_project.rb new file mode 100644 index 00000000000..fcffc98b47a --- /dev/null +++ b/db/migrate/20160407120251_add_images_enabled_for_project.rb @@ -0,0 +1,6 @@ +# rubocop:disable all +class AddImagesEnabledForProject < ActiveRecord::Migration + def change + add_column :projects, :container_registry_enabled, :boolean + end +end diff --git a/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb b/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb index ebfa4bcbc7b..920d4d41110 100644 --- a/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb +++ b/db/migrate/20160412140240_add_repository_checks_enabled_setting.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddRepositoryChecksEnabledSetting < ActiveRecord::Migration def change add_column :application_settings, :repository_checks_enabled, :boolean, default: true diff --git a/db/migrate/20160412173416_add_fields_to_ci_commit.rb b/db/migrate/20160412173416_add_fields_to_ci_commit.rb index 125956a3ddd..00162af5cda 100644 --- a/db/migrate/20160412173416_add_fields_to_ci_commit.rb +++ b/db/migrate/20160412173416_add_fields_to_ci_commit.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddFieldsToCiCommit < ActiveRecord::Migration def change add_column :ci_commits, :status, :string diff --git a/db/migrate/20160412173417_update_ci_commit.rb b/db/migrate/20160412173417_update_ci_commit.rb index fd92444dbac..858faeb060e 100644 --- a/db/migrate/20160412173417_update_ci_commit.rb +++ b/db/migrate/20160412173417_update_ci_commit.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class UpdateCiCommit < ActiveRecord::Migration # This migration can be run online, but needs to be executed for the second time after restarting Unicorn workers # Otherwise Offline migration should be used. diff --git a/db/migrate/20160412173418_add_ci_commit_indexes.rb b/db/migrate/20160412173418_add_ci_commit_indexes.rb index 603d4a41610..414f1f8279f 100644 --- a/db/migrate/20160412173418_add_ci_commit_indexes.rb +++ b/db/migrate/20160412173418_add_ci_commit_indexes.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddCiCommitIndexes < ActiveRecord::Migration disable_ddl_transaction! diff --git a/db/migrate/20160413115152_add_token_to_web_hooks.rb b/db/migrate/20160413115152_add_token_to_web_hooks.rb index f04225068cd..628b1d51b30 100644 --- a/db/migrate/20160413115152_add_token_to_web_hooks.rb +++ b/db/migrate/20160413115152_add_token_to_web_hooks.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddTokenToWebHooks < ActiveRecord::Migration def change add_column :web_hooks, :token, :string diff --git a/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb b/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb index d493044c67b..b53b9bc6c3d 100644 --- a/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb +++ b/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddSharedRunnersTextToApplicationSettings < ActiveRecord::Migration def change add_column :application_settings, :shared_runners_text, :text diff --git a/db/migrate/20160416180807_add_award_emoji.rb b/db/migrate/20160416180807_add_award_emoji.rb new file mode 100644 index 00000000000..a3bee9b1bc6 --- /dev/null +++ b/db/migrate/20160416180807_add_award_emoji.rb @@ -0,0 +1,15 @@ +# rubocop:disable all +class AddAwardEmoji < ActiveRecord::Migration + def change + create_table :award_emoji do |t| + t.string :name + t.references :user + t.references :awardable, polymorphic: true + + t.timestamps + end + + add_index :award_emoji, :user_id + add_index :award_emoji, [:awardable_type, :awardable_id] + end +end diff --git a/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb new file mode 100644 index 00000000000..c226bc11f6c --- /dev/null +++ b/db/migrate/20160416182152_convert_award_note_to_emoji_award.rb @@ -0,0 +1,10 @@ +# rubocop:disable all +class ConvertAwardNoteToEmojiAward < ActiveRecord::Migration + def change + def up + execute "INSERT INTO award_emoji (awardable_type, awardable_id, user_id, name, created_at, updated_at) (SELECT noteable_type, noteable_id, author_id, note, created_at, updated_at FROM notes WHERE is_award = true)" + + execute "DELETE FROM notes WHERE is_award = true" + end + end +end diff --git a/db/migrate/20160416190505_remove_note_is_award.rb b/db/migrate/20160416190505_remove_note_is_award.rb new file mode 100644 index 00000000000..dd24917feb9 --- /dev/null +++ b/db/migrate/20160416190505_remove_note_is_award.rb @@ -0,0 +1,6 @@ +# rubocop:disable all +class RemoveNoteIsAward < ActiveRecord::Migration + def change + remove_column :notes, :is_award, :boolean + end +end diff --git a/db/migrate/20160419120017_add_metrics_packet_size.rb b/db/migrate/20160419120017_add_metrics_packet_size.rb index 78c163d62ac..c759427c590 100644 --- a/db/migrate/20160419120017_add_metrics_packet_size.rb +++ b/db/migrate/20160419120017_add_metrics_packet_size.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class AddMetricsPacketSize < ActiveRecord::Migration def change add_column :application_settings, :metrics_packet_size, :integer, default: 1 diff --git a/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb new file mode 100644 index 00000000000..69d64ccd006 --- /dev/null +++ b/db/migrate/20160419122101_add_only_allow_merge_if_build_succeeds_to_projects.rb @@ -0,0 +1,15 @@ +class AddOnlyAllowMergeIfBuildSucceedsToProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + def up + add_column_with_default(:projects, + :only_allow_merge_if_build_succeeds, + :boolean, + default: false) + end + + def down + remove_column(:projects, :only_allow_merge_if_build_succeeds) + end +end diff --git a/db/migrate/20160421130527_disable_repository_checks.rb b/db/migrate/20160421130527_disable_repository_checks.rb index 808a4b93c7c..7e65ddc45e7 100644 --- a/db/migrate/20160421130527_disable_repository_checks.rb +++ b/db/migrate/20160421130527_disable_repository_checks.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class DisableRepositoryChecks < ActiveRecord::Migration def up change_column_default :application_settings, :repository_checks_enabled, false diff --git a/db/migrate/20160425045124_create_u2f_registrations.rb b/db/migrate/20160425045124_create_u2f_registrations.rb new file mode 100644 index 00000000000..72cbe98ebba --- /dev/null +++ b/db/migrate/20160425045124_create_u2f_registrations.rb @@ -0,0 +1,14 @@ +# rubocop:disable all +class CreateU2fRegistrations < ActiveRecord::Migration + def change + create_table :u2f_registrations do |t| + t.text :certificate + t.string :key_handle, index: true + t.string :public_key + t.integer :counter + t.references :user, index: true, foreign_key: true + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb b/db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb new file mode 100644 index 00000000000..bf50616656c --- /dev/null +++ b/db/migrate/20160504091942_add_disabled_oauth_sign_in_sources_to_application_settings.rb @@ -0,0 +1,6 @@ +# rubocop:disable all +class AddDisabledOauthSignInSourcesToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :disabled_oauth_sign_in_sources, :text + end +end diff --git a/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb b/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb new file mode 100644 index 00000000000..c60892a6279 --- /dev/null +++ b/db/migrate/20160504112519_add_run_untagged_to_ci_runner.rb @@ -0,0 +1,14 @@ +# rubocop:disable all +class AddRunUntaggedToCiRunner < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + def up + add_column_with_default(:ci_runners, :run_untagged, :boolean, + default: true, allow_null: false) + end + + def down + remove_column(:ci_runners, :run_untagged) + end +end diff --git a/db/migrate/20160508194200_remove_wall_enabled_from_projects.rb b/db/migrate/20160508194200_remove_wall_enabled_from_projects.rb index aa560bc0f0c..6792ffc957a 100644 --- a/db/migrate/20160508194200_remove_wall_enabled_from_projects.rb +++ b/db/migrate/20160508194200_remove_wall_enabled_from_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class RemoveWallEnabledFromProjects < ActiveRecord::Migration def change remove_column :projects, :wall_enabled, :boolean, default: true, null: false diff --git a/db/migrate/20160508215820_add_type_to_notes.rb b/db/migrate/20160508215820_add_type_to_notes.rb new file mode 100644 index 00000000000..c1d07c9363f --- /dev/null +++ b/db/migrate/20160508215820_add_type_to_notes.rb @@ -0,0 +1,6 @@ +# rubocop:disable all +class AddTypeToNotes < ActiveRecord::Migration + def change + add_column :notes, :type, :string + end +end diff --git a/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb b/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb new file mode 100644 index 00000000000..6dd958ff4a0 --- /dev/null +++ b/db/migrate/20160508221410_set_type_on_legacy_diff_notes.rb @@ -0,0 +1,6 @@ +# rubocop:disable all +class SetTypeOnLegacyDiffNotes < ActiveRecord::Migration + def change + execute "UPDATE notes SET type = 'LegacyDiffNote' WHERE line_code IS NOT NULL" + end +end diff --git a/db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb b/db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb new file mode 100644 index 00000000000..b6a5bea79b6 --- /dev/null +++ b/db/migrate/20160509201028_add_health_check_access_token_to_application_settings.rb @@ -0,0 +1,6 @@ +# rubocop:disable all +class AddHealthCheckAccessTokenToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :health_check_access_token, :string + end +end diff --git a/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb b/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb new file mode 100644 index 00000000000..8c96353b850 --- /dev/null +++ b/db/migrate/20160516174813_add_send_user_confirmation_email_to_application_settings.rb @@ -0,0 +1,13 @@ +# rubocop:disable all +class AddSendUserConfirmationEmailToApplicationSettings < ActiveRecord::Migration + def up + add_column :application_settings, :send_user_confirmation_email, :boolean, default: false + + #Sets confirmation email to true by default on existing installations. + execute "UPDATE application_settings SET send_user_confirmation_email=true" + end + + def down + remove_column :application_settings, :send_user_confirmation_email + end +end diff --git a/db/migrate/20160525205328_remove_main_language_from_projects.rb b/db/migrate/20160525205328_remove_main_language_from_projects.rb new file mode 100644 index 00000000000..dc4ceacddb1 --- /dev/null +++ b/db/migrate/20160525205328_remove_main_language_from_projects.rb @@ -0,0 +1,22 @@ +# rubocop:disable all +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveMainLanguageFromProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + remove_column :projects, :main_language + end +end diff --git a/db/migrate/20160527020117_remove_notification_settings_for_deleted_projects.rb b/db/migrate/20160527020117_remove_notification_settings_for_deleted_projects.rb new file mode 100644 index 00000000000..3e26be7c09c --- /dev/null +++ b/db/migrate/20160527020117_remove_notification_settings_for_deleted_projects.rb @@ -0,0 +1,14 @@ +# rubocop:disable all +class RemoveNotificationSettingsForDeletedProjects < ActiveRecord::Migration + def up + execute <<-SQL + DELETE FROM notification_settings + WHERE notification_settings.source_type = 'Project' + AND NOT EXISTS ( + SELECT * + FROM projects + WHERE projects.id = notification_settings.source_id + ) + SQL + end +end diff --git a/db/migrate/20160528043124_add_users_state_index.rb b/db/migrate/20160528043124_add_users_state_index.rb new file mode 100644 index 00000000000..6419d2ae71d --- /dev/null +++ b/db/migrate/20160528043124_add_users_state_index.rb @@ -0,0 +1,10 @@ +# rubocop:disable all +class AddUsersStateIndex < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + def change + add_concurrent_index :users, :state + end +end diff --git a/db/migrate/20160530150109_add_container_registry_token_expire_delay_to_application_settings.rb b/db/migrate/20160530150109_add_container_registry_token_expire_delay_to_application_settings.rb new file mode 100644 index 00000000000..d811fd5271e --- /dev/null +++ b/db/migrate/20160530150109_add_container_registry_token_expire_delay_to_application_settings.rb @@ -0,0 +1,10 @@ +# rubocop:disable all +# This is ONLINE migration + +class AddContainerRegistryTokenExpireDelayToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + def change + add_column :application_settings, :container_registry_token_expire_delay, :integer, default: 5 + end +end diff --git a/db/migrate/20160603075128_add_has_external_issue_tracker_to_projects.rb b/db/migrate/20160603075128_add_has_external_issue_tracker_to_projects.rb new file mode 100644 index 00000000000..be295f0181d --- /dev/null +++ b/db/migrate/20160603075128_add_has_external_issue_tracker_to_projects.rb @@ -0,0 +1,10 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddHasExternalIssueTrackerToProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + def change + add_column(:projects, :has_external_issue_tracker, :boolean) + end +end diff --git a/db/migrate/20160603180330_remove_duplicated_notification_settings.rb b/db/migrate/20160603180330_remove_duplicated_notification_settings.rb new file mode 100644 index 00000000000..4f4f58b1619 --- /dev/null +++ b/db/migrate/20160603180330_remove_duplicated_notification_settings.rb @@ -0,0 +1,33 @@ +# rubocop:disable all +class RemoveDuplicatedNotificationSettings < ActiveRecord::Migration + def up + duplicates = exec_query(%Q{ + SELECT user_id, source_type, source_id + FROM notification_settings + GROUP BY user_id, source_type, source_id + HAVING COUNT(*) > 1 + }) + + duplicates.each do |row| + uid = row['user_id'] + stype = connection.quote(row['source_type']) + sid = row['source_id'] + + execute(%Q{ + DELETE FROM notification_settings + WHERE user_id = #{uid} + AND source_type = #{stype} + AND source_id = #{sid} + AND id != ( + SELECT id FROM ( + SELECT min(id) AS id + FROM notification_settings + WHERE user_id = #{uid} + AND source_type = #{stype} + AND source_id = #{sid} + ) min_ids + ) + }) + end + end +end diff --git a/db/migrate/20160603182247_add_index_to_notification_settings.rb b/db/migrate/20160603182247_add_index_to_notification_settings.rb new file mode 100644 index 00000000000..f6ae26d555f --- /dev/null +++ b/db/migrate/20160603182247_add_index_to_notification_settings.rb @@ -0,0 +1,10 @@ +# rubocop:disable all +class AddIndexToNotificationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + def change + add_concurrent_index :notification_settings, [:user_id, :source_id, :source_type], { unique: true, name: "index_notifications_on_user_id_and_source_id_and_source_type" } + end +end diff --git a/db/migrate/20160608155312_add_after_sign_up_text_to_application_settings.rb b/db/migrate/20160608155312_add_after_sign_up_text_to_application_settings.rb new file mode 100644 index 00000000000..3c5d2ad910e --- /dev/null +++ b/db/migrate/20160608155312_add_after_sign_up_text_to_application_settings.rb @@ -0,0 +1,6 @@ +# rubocop:disable all +class AddAfterSignUpTextToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :after_sign_up_text, :text + end +end diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb index 14d7e84d856..be3501c4c2e 100644 --- a/db/migrate/limits_to_mysql.rb +++ b/db/migrate/limits_to_mysql.rb @@ -1,3 +1,4 @@ +# rubocop:disable all class LimitsToMysql < ActiveRecord::Migration def up return unless ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/ diff --git a/db/schema.rb b/db/schema.rb index 89dba1318fa..aac327797e7 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: 20160508194200) do +ActiveRecord::Schema.define(version: 20160608155312) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -43,43 +43,48 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.datetime "created_at" t.datetime "updated_at" t.string "home_page_url" - t.integer "default_branch_protection", default: 2 + t.integer "default_branch_protection", default: 2 t.text "restricted_visibility_levels" - t.boolean "version_check_enabled", default: true - t.integer "max_attachment_size", default: 10, null: false + t.boolean "version_check_enabled", default: true + t.integer "max_attachment_size", default: 10, null: false t.integer "default_project_visibility" t.integer "default_snippet_visibility" t.text "restricted_signup_domains" - t.boolean "user_oauth_applications", default: true + t.boolean "user_oauth_applications", default: true t.string "after_sign_out_path" - t.integer "session_expire_delay", default: 10080, null: false + t.integer "session_expire_delay", default: 10080, null: false t.text "import_sources" t.text "help_page_text" t.string "admin_notification_email" - t.boolean "shared_runners_enabled", default: true, null: false - t.integer "max_artifacts_size", default: 100, null: false + t.boolean "shared_runners_enabled", default: true, null: false + t.integer "max_artifacts_size", default: 100, null: false t.string "runners_registration_token" - t.boolean "require_two_factor_authentication", default: false - t.integer "two_factor_grace_period", default: 48 - t.boolean "metrics_enabled", default: false - t.string "metrics_host", default: "localhost" - t.integer "metrics_pool_size", default: 16 - t.integer "metrics_timeout", default: 10 - t.integer "metrics_method_call_threshold", default: 10 - t.boolean "recaptcha_enabled", default: false + t.boolean "require_two_factor_authentication", default: false + t.integer "two_factor_grace_period", default: 48 + t.boolean "metrics_enabled", default: false + t.string "metrics_host", default: "localhost" + t.integer "metrics_pool_size", default: 16 + t.integer "metrics_timeout", default: 10 + t.integer "metrics_method_call_threshold", default: 10 + t.boolean "recaptcha_enabled", default: false t.string "recaptcha_site_key" t.string "recaptcha_private_key" - t.integer "metrics_port", default: 8089 - t.integer "metrics_sample_interval", default: 15 - t.boolean "sentry_enabled", default: false - t.string "sentry_dsn" - t.boolean "akismet_enabled", default: false + t.integer "metrics_port", default: 8089 + t.boolean "akismet_enabled", default: false t.string "akismet_api_key" - t.boolean "email_author_in_body", default: false + t.integer "metrics_sample_interval", default: 15 + t.boolean "sentry_enabled", default: false + t.string "sentry_dsn" + t.boolean "email_author_in_body", default: false t.integer "default_group_visibility" - t.boolean "repository_checks_enabled", default: false + t.boolean "repository_checks_enabled", default: false t.text "shared_runners_text" - t.integer "metrics_packet_size", default: 1 + t.integer "metrics_packet_size", default: 1 + t.text "disabled_oauth_sign_in_sources" + t.string "health_check_access_token" + t.boolean "send_user_confirmation_email", default: false + t.integer "container_registry_token_expire_delay", default: 5 + t.text "after_sign_up_text" end create_table "audit_events", force: :cascade do |t| @@ -96,6 +101,18 @@ ActiveRecord::Schema.define(version: 20160508194200) do add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree + create_table "award_emoji", force: :cascade do |t| + t.string "name" + t.integer "user_id" + t.integer "awardable_id" + t.string "awardable_type" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "award_emoji", ["awardable_type", "awardable_id"], name: "index_award_emoji_on_awardable_type_and_awardable_id", using: :btree + add_index "award_emoji", ["user_id"], name: "index_award_emoji_on_user_id", using: :btree + create_table "broadcast_messages", force: :cascade do |t| t.text "message", null: false t.datetime "starts_at" @@ -267,6 +284,7 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "revision" t.string "platform" t.string "architecture" + t.boolean "run_untagged", default: true, null: false end add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} @@ -428,8 +446,8 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.integer "updated_by_id" t.boolean "confidential", default: false t.datetime "deleted_at" - t.integer "moved_to_id" t.date "due_date" + t.integer "moved_to_id" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -480,8 +498,10 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.datetime "updated_at" t.boolean "template", default: false t.string "description" + t.integer "priority" end + add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree create_table "lfs_objects", force: :cascade do |t| @@ -633,14 +653,13 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.boolean "system", default: false, null: false t.text "st_diff" t.integer "updated_by_id" - t.boolean "is_award", default: false, null: false + t.string "type" end add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree - add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"} add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree @@ -659,6 +678,7 @@ ActiveRecord::Schema.define(version: 20160508194200) do end add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree + add_index "notification_settings", ["user_id", "source_id", "source_type"], name: "index_notifications_on_user_id_and_source_id_and_source_type", unique: true, using: :btree add_index "notification_settings", ["user_id"], name: "index_notification_settings_on_user_id", using: :btree create_table "oauth_access_grants", force: :cascade do |t| @@ -716,8 +736,8 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.integer "project_id" t.text "data" t.text "encrypted_credentials" - t.text "encrypted_credentials_iv" - t.text "encrypted_credentials_salt" + t.string "encrypted_credentials_iv" + t.string "encrypted_credentials_salt" end create_table "projects", force: :cascade do |t| @@ -727,38 +747,40 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.datetime "created_at" t.datetime "updated_at" t.integer "creator_id" - t.boolean "issues_enabled", default: true, null: false - t.boolean "merge_requests_enabled", default: true, null: false - t.boolean "wiki_enabled", default: true, null: false + t.boolean "issues_enabled", default: true, null: false + t.boolean "merge_requests_enabled", default: true, null: false + t.boolean "wiki_enabled", default: true, null: false t.integer "namespace_id" - t.string "issues_tracker", default: "gitlab", null: false + t.string "issues_tracker", default: "gitlab", null: false t.string "issues_tracker_id" - t.boolean "snippets_enabled", default: true, null: false + t.boolean "snippets_enabled", default: true, null: false t.datetime "last_activity_at" t.string "import_url" - t.integer "visibility_level", default: 0, null: false - t.boolean "archived", default: false, null: false + t.integer "visibility_level", default: 0, null: false + t.boolean "archived", default: false, null: false t.string "avatar" t.string "import_status" - t.float "repository_size", default: 0.0 - t.integer "star_count", default: 0, null: false + t.float "repository_size", default: 0.0 + t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" - t.integer "commit_count", default: 0 + t.integer "commit_count", default: 0 t.text "import_error" t.integer "ci_id" - t.boolean "builds_enabled", default: true, null: false - t.boolean "shared_runners_enabled", default: true, null: false + t.boolean "builds_enabled", default: true, null: false + t.boolean "shared_runners_enabled", default: true, null: false t.string "runners_token" t.string "build_coverage_regex" - t.boolean "build_allow_git_fetch", default: true, null: false - t.integer "build_timeout", default: 3600, null: false - t.boolean "pending_delete", default: false - t.boolean "public_builds", default: true, null: false - t.string "main_language" - t.integer "pushes_since_gc", default: 0 + t.boolean "build_allow_git_fetch", default: true, null: false + t.integer "build_timeout", default: 3600, null: false + t.boolean "pending_delete", default: false + t.boolean "public_builds", default: true, null: false + t.integer "pushes_since_gc", default: 0 t.boolean "last_repository_check_failed" t.datetime "last_repository_check_at" + t.boolean "container_registry_enabled" + t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false + t.boolean "has_external_issue_tracker" end add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree @@ -815,9 +837,9 @@ ActiveRecord::Schema.define(version: 20160508194200) do t.string "type" t.string "title" t.integer "project_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "active", null: false + t.datetime "created_at" + t.datetime "updated_at" + t.boolean "active", default: false, null: false t.text "properties" t.boolean "template", default: false t.boolean "push_events", default: true @@ -924,6 +946,19 @@ ActiveRecord::Schema.define(version: 20160508194200) do add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree + create_table "u2f_registrations", force: :cascade do |t| + t.text "certificate" + t.string "key_handle" + t.string "public_key" + t.integer "counter" + t.integer "user_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree + add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree + create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false @@ -995,6 +1030,7 @@ ActiveRecord::Schema.define(version: 20160508194200) do add_index "users", ["name"], name: "index_users_on_name", using: :btree add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree + add_index "users", ["state"], name: "index_users_on_state", using: :btree add_index "users", ["username"], name: "index_users_on_username", using: :btree add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"} @@ -1030,4 +1066,5 @@ ActiveRecord::Schema.define(version: 20160508194200) do add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree + add_foreign_key "u2f_registrations", "users" end diff --git a/doc/README.md b/doc/README.md index e358da1c424..d1345ab2493 100644 --- a/doc/README.md +++ b/doc/README.md @@ -13,6 +13,7 @@ - [Profile Settings](profile/README.md) - [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat. - [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. +- [Container Registry](container_registry/README.md) Learn how to use GitLab Container Registry. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. @@ -41,8 +42,10 @@ - [Git LFS configuration](workflow/lfs/lfs_administration.md) - [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast. - [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics +- [Monitoring uptime](monitoring/health_check.md) Check the server status using the health check endpoint - [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs - [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability +- [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab ## Contributor documentation diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md new file mode 100644 index 00000000000..7870669fa77 --- /dev/null +++ b/doc/administration/container_registry.md @@ -0,0 +1,375 @@ +# GitLab Container Registry Administration + +> **Note:** +This feature was [introduced][ce-4040] in GitLab 8.8. + +With the Docker Container Registry integrated into GitLab, every project can +have its own space to store its Docker images. + +You can read more about Docker Registry at https://docs.docker.com/registry/introduction/. + +--- + +<!-- START doctoc generated TOC please keep comment here to allow auto update --> +<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Enable the Container Registry](#enable-the-container-registry) +- [Container Registry domain configuration](#container-registry-domain-configuration) + - [Configure Container Registry under an existing GitLab domain](#configure-container-registry-under-an-existing-gitlab-domain) + - [Configure Container Registry under its own domain](#configure-container-registry-under-its-own-domain) +- [Disable Container Registry site-wide](#disable-container-registry-site-wide) +- [Disable Container Registry per project](#disable-container-registry-per-project) +- [Disable Container Registry for new projects site-wide](#disable-container-registry-for-new-projects-site-wide) +- [Container Registry storage path](#container-registry-storage-path) +- [Storage limitations](#storage-limitations) +- [Changelog](#changelog) + +<!-- END doctoc generated TOC please keep comment here to allow auto update --> + +## Enable the Container Registry + +**Omnibus GitLab installations** + +All you have to do is configure the domain name under which the Container +Registry will listen to. Read [#container-registry-domain-configuration](#container-registry-domain-configuration) +and pick one of the two options that fits your case. + +>**Note:** +The container Registry works under HTTPS by default. Using HTTP is possible +but not recommended and out of the scope of this document. +Read the [insecure Registry documentation][docker-insecure] if you want to +implement this. + +--- + +**Installations from source** + +If you have installed GitLab from source: + +1. You will have to [install Docker Registry][registry-deploy] by yourself. +1. After the installation is complete, you will have to configure the Registry's + settings in `gitlab.yml` in order to enable it. +1. Use the sample NGINX configuration file that is found under + [`lib/support/nginx/registry-ssl`][registry-ssl] and edit it to match the + `host`, `port` and TLS certs paths. + +The contents of `gitlab.yml` are: + +``` +registry: + enabled: true + host: registry.gitlab.example.com + port: 5005 + api_url: http://localhost:5000/ + key: config/registry.key + path: shared/registry + issuer: gitlab-issuer +``` + +where: + +| Parameter | Description | +| --------- | ----------- | +| `enabled` | `true` or `false`. Enables the Registry in GitLab. By default this is `false`. | +| `host` | The host URL under which the Registry will run and the users will be able to use. | +| `port` | The port under which the external Registry domain will listen on. | +| `api_url` | The internal API URL under which the Registry is exposed to. It defaults to `http://localhost:5000`. | +| `key` | The private key location that is a pair of Registry's `rootcertbundle`. Read the [token auth configuration documentation][token-config]. | +| `path` | This should be the same directory like specified in Registry's `rootdirectory`. Read the [storage configuration documentation][storage-config]. This path needs to be readable by the GitLab user, the web-server user and the Registry user. Read more in [#container-registry-storage-path](#container-registry-storage-path). | +| `issuer` | This should be the same value as configured in Registry's `issuer`. Read the [token auth configuration documentation][token-config]. | + +>**Note:** +GitLab does not ship with a Registry init file. Hence, [restarting GitLab][restart gitlab] +will not restart the Registry should you modify its settings. Read the upstream +documentation on how to achieve that. + +## Container Registry domain configuration + +There are two ways you can configure the Registry's external domain. + +- Either [use the existing GitLab domain][existing-domain] where in that case + the Registry will have to listen on a port and reuse GitLab's TLS certificate, +- or [use a completely separate domain][new-domain] with a new TLS certificate + for that domain. + +Since the container Registry requires a TLS certificate, in the end it all boils +down to how easy or pricey is to get a new one. + +Please take this into consideration before configuring the Container Registry +for the first time. + +### Configure Container Registry under an existing GitLab domain + +If the Registry is configured to use the existing GitLab domain, you can +expose the Registry on a port so that you can reuse the existing GitLab TLS +certificate. + +Assuming that the GitLab domain is `https://gitlab.example.com` and the port the +Registry is exposed to the outside world is `4567`, here is what you need to set +in `gitlab.rb` or `gitlab.yml` if you are using Omnibus GitLab or installed +GitLab from source respectively. + +--- + +**Omnibus GitLab installations** + +1. Your `/etc/gitlab/gitlab.rb` should contain the Registry URL as well as the + path to the existing TLS certificate and key used by GitLab: + + ```ruby + registry_external_url 'https://gitlab.example.com:4567' + ``` + + Note how the `registry_external_url` is listening on HTTPS under the + existing GitLab URL, but on a different port. + + If your TLS certificate is not in `/etc/gitlab/ssl/gitlab.example.com.crt` + and key not in `/etc/gitlab/ssl/gitlab.example.com.key` uncomment the lines + below: + + ```ruby + registry_nginx['ssl_certificate'] = "/path/to/certificate.pem" + registry_nginx['ssl_certificate_key'] = "/path/to/certificate.key" + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +--- + +**Installations from source** + +1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and + configure it with the following settings: + + ``` + registry: + enabled: true + host: gitlab.example.com + port: 4567 + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. +1. Make the relevant changes in NGINX as well (domain, port, TLS certificates path). + +--- + +Users should now be able to login to the Container Registry with their GitLab +credentials using: + +```bash +docker login gitlab.example.com:4567 +``` + +### Configure Container Registry under its own domain + +If the Registry is configured to use its own domain, you will need a TLS +certificate for that specific domain (e.g., `registry.example.com`) or maybe +a wildcard certificate if hosted under a subdomain of your existing GitLab +domain (e.g., `registry.gitlab.example.com`). + +Let's assume that you want the container Registry to be accessible at +`https://registry.gitlab.example.com`. + +--- + +**Omnibus GitLab installations** + +1. Place your TLS certificate and key in + `/etc/gitlab/ssl/registry.gitlab.example.com.crt` and + `/etc/gitlab/ssl/registry.gitlab.example.com.key` and make sure they have + correct permissions: + + ```bash + chmod 600 /etc/gitlab/ssl/registry.gitlab.example.com.* + ``` + +1. Once the TLS certificate is in place, edit `/etc/gitlab/gitlab.rb` with: + + ```ruby + registry_external_url 'https://registry.gitlab.example.com' + ``` + + Note how the `registry_external_url` is listening on HTTPS. + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +> **Note:** +If you have a [wildcard certificate][], you need to specify the path to the +certificate in addition to the URL, in this case `/etc/gitlab/gitlab.rb` will +look like: +> +```ruby +registry_nginx['ssl_certificate'] = "/etc/gitlab/ssl/certificate.pem" +registry_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/certificate.key" +``` + +--- + +**Installations from source** + +1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and + configure it with the following settings: + + ``` + registry: + enabled: true + host: registry.gitlab.example.com + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. +1. Make the relevant changes in NGINX as well (domain, port, TLS certificates path). + +--- + +Users should now be able to login to the Container Registry using their GitLab +credentials: + +```bash +docker login registry.gitlab.example.com +``` + +## Disable Container Registry site-wide + +>**Note:** +Disabling the Registry in the Rails GitLab application as set by the following +steps, will not remove any existing Docker images. This is handled by the +Registry application itself. + +**Omnibus GitLab** + +1. Open `/etc/gitlab/gitlab.rb` and set `registry['enable']` to `false`: + + ```ruby + registry['enable'] = false + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +--- + +**Installations from source** + +1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and + set `enabled` to `false`: + + ``` + registry: + enabled: false + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + +## Disable Container Registry per project + +If Registry is enabled in your GitLab instance, but you don't need it for your +project, you can disable it from your project's settings. Read the user guide +on how to achieve that. + +## Disable Container Registry for new projects site-wide + +If the Container Registry is enabled, then it will be available on all new +projects. To disable this function and let the owners of a project to enable +the Container Registry by themselves, follow the steps below. + +--- + +**Omnibus GitLab installations** + +1. Edit `/etc/gitlab/gitlab.rb` and add the following line: + + ```ruby + gitlab_rails['gitlab_default_projects_features_container_registry'] = false + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +--- + +**Installations from source** + +1. Open `/home/git/gitlab/config/gitlab.yml`, find the `default_projects_features` + entry and configure it so that `container_registry` is set to `false`: + + ``` + ## Default project features settings + default_projects_features: + issues: true + merge_requests: true + wiki: true + snippets: false + builds: true + container_registry: false + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + +## Container Registry storage path + +To change the storage path where Docker images will be stored, follow the +steps below. + +This path is accessible to: + +- the user running the Container Registry daemon, +- the user running GitLab + +> **Warning** You should confirm that all GitLab, Registry and web server users +have access to this directory. + +--- + +**Omnibus GitLab installations** + +The default location where images are stored in Omnibus, is +`/var/opt/gitlab/gitlab-rails/shared/registry`. To change it: + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + gitlab_rails['registry_path'] = "/path/to/registry/storage" + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +--- + +**Installations from source** + +The default location where images are stored in source installations, is +`/home/git/gitlab/shared/registry`. To change it: + +1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and + change the `path` setting: + + ``` + registry: + path: shared/registry + ``` + +1. Save the file and [restart GitLab][] for the changes to take effect. + +## Storage limitations + +Currently, there is no storage limitation, which means a user can upload an +infinite amount of Docker images with arbitrary sizes. This setting will be +configurable in future releases. + +## Changelog + +**GitLab 8.8 ([source docs][8-8-docs])** + +- GitLab Container Registry feature was introduced. + +[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure +[restart gitlab]: restart_gitlab.md#installations-from-source +[wildcard certificate]: https://en.wikipedia.org/wiki/Wildcard_certificate +[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 +[docker-insecure]: https://docs.docker.com/registry/insecure/ +[registry-deploy]: https://docs.docker.com/registry/deploying/ +[storage-config]: https://docs.docker.com/registry/configuration/#storage +[token-config]: https://docs.docker.com/registry/configuration/#token +[8-8-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/doc/administration/container_registry.md +[registry-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/registry-ssl +[existing-domain]: #configure-container-registry-under-an-existing-gitlab-domain +[new-domain]: #configure-container-registry-under-its-own-domain diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index 43ab153d76d..7f53915a4d7 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -58,4 +58,4 @@ to the naming scheme `GITLAB_#{name in 1_settings.rb in upper case}`. It's possible to preconfigure the GitLab docker image by adding the environment variable `GITLAB_OMNIBUS_CONFIG` to the `docker run` command. -For more information see the ['preconfigure-docker-container' section in the Omnibus documentation](http://doc.gitlab.com/omnibus/docker/#preconfigure-docker-container). +For more information see the ['preconfigure-docker-container' section in the Omnibus documentation](http://docs.gitlab.com/omnibus/docker/#preconfigure-docker-container). diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md index 43d85ffb775..d74a786ac24 100644 --- a/doc/administration/high_availability/README.md +++ b/doc/administration/high_availability/README.md @@ -19,6 +19,8 @@ Components/Servers Required: - 2 servers/virtual machines (one active/one passive) +![Active/Passive HA Diagram](../img/high_availability/active-passive-diagram.png) + ### Active/Active This architecture scales easily because all application servers handle @@ -26,6 +28,8 @@ user requests simultaneously. The database, Redis, and GitLab application are all deployed on separate servers. The configuration is **only** highly-available if the database, Redis and storage are also configured as such. +![Active/Active HA Diagram](../img/high_availability/active-active-diagram.png) + **Steps to configure active/active:** 1. [Configure the database](database.md) diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index b1fe34ed9a1..136f570ac27 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -60,4 +60,4 @@ Read more on high-availability configuration: configure custom domains with custom SSL, which would not be possible if SSL was terminated at the load balancer. -[gitlab-pages]: http://doc.gitlab.com/ee/pages/administration.html +[gitlab-pages]: http://docs.gitlab.com/ee/pages/administration.html diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index e4e124e200a..537f4f3501d 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -2,8 +2,8 @@ ## Required NFS Server features -**File locking**: GitLab **requires** file locking which is only supported -natively in NFS version 4. NFSv3 also supports locking as long as +**File locking**: GitLab **requires** advisory file locking, which is only +supported natively in NFS version 4. NFSv3 also supports locking as long as Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not specifically test NFSv3. @@ -113,4 +113,4 @@ Read more on high-availability configuration: 1. [Configure the GitLab application servers](gitlab.md) 1. [Configure the load balancers](load_balancer.md) -[udp-log-shipping]: http://doc.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only "UDP log shipping" +[udp-log-shipping]: http://docs.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only "UDP log shipping" diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index d89a1e582ca..f6153216f33 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -26,7 +26,7 @@ that runs Redis. ```ruby external_url 'https://gitlab.example.com' - # Disable all components except PostgreSQL + # Disable all components except Redis redis['enable'] = true bootstrap['enable'] = false nginx['enable'] = false diff --git a/doc/administration/img/high_availability/active-active-diagram.png b/doc/administration/img/high_availability/active-active-diagram.png Binary files differnew file mode 100644 index 00000000000..81259e0ae93 --- /dev/null +++ b/doc/administration/img/high_availability/active-active-diagram.png diff --git a/doc/administration/img/high_availability/active-passive-diagram.png b/doc/administration/img/high_availability/active-passive-diagram.png Binary files differnew file mode 100644 index 00000000000..f69ff1d0357 --- /dev/null +++ b/doc/administration/img/high_availability/active-passive-diagram.png diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md index 3411e4af6a7..4172b604cec 100644 --- a/doc/administration/repository_checks.md +++ b/doc/administration/repository_checks.md @@ -5,7 +5,7 @@ This feature was [introduced][ce-3232] in GitLab 8.7. It is OFF by default because it still causes too many false alarms. Git has a built-in mechanism, [git fsck][git-fsck], to verify the -integrity of all data commited to a repository. GitLab administrators +integrity of all data committed to a repository. GitLab administrators can trigger such a check for a project via the project page under the admin panel. The checks run asynchronously so it may take a few minutes before the check result is visible on the project admin page. If the @@ -41,4 +41,4 @@ alarms you can choose to clear ALL repository check states from the --- [ce-3232]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3232 "Auto git fsck" -[git-fsck]: https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html "git fsck documentation"
\ No newline at end of file +[git-fsck]: https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html "git fsck documentation" diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md index 134a7583762..a776cd3f05e 100644 --- a/doc/administration/troubleshooting/sidekiq.md +++ b/doc/administration/troubleshooting/sidekiq.md @@ -150,6 +150,14 @@ To output a backtrace from all threads at once: apply all thread bt ``` +Once you're done debugging with `gdb`, be sure to detach from the process and +exit: + +``` +detach +exit +``` + ## Check for blocking queries Sometimes the speed at which Sidekiq processes jobs can be so fast that it can diff --git a/doc/api/README.md b/doc/api/README.md index ff039f1886f..27c5962decf 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -33,7 +33,7 @@ following locations: - [Build triggers](build_triggers.md) - [Build Variables](build_variables.md) - [Runners](runners.md) -- [Licenses](licenses.md) +- [Open source license templates](licenses.md) ## Authentication diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md index 4a12e962b62..0881a7d7a90 100644 --- a/doc/api/build_triggers.md +++ b/doc/api/build_triggers.md @@ -101,8 +101,18 @@ DELETE /projects/:id/triggers/:token | Attribute | Type | required | Description | |-----------|---------|----------|--------------------------| | `id` | integer | yes | The ID of a project | -| `token` | string | yes | The `token` of a project | +| `token` | string | yes | The `token` of a trigger | ``` curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d" ``` + +```json +{ + "created_at": "2015-12-23T16:25:56.760Z", + "deleted_at": "2015-12-24T12:32:20.100Z", + "last_used": null, + "token": "7b9148c158980bbd9bcea92c17522d", + "updated_at": "2015-12-24T12:32:20.100Z" +} +``` diff --git a/doc/api/builds.md b/doc/api/builds.md index 4c0a47d1ea0..5669bd0cdda 100644 --- a/doc/api/builds.md +++ b/doc/api/builds.md @@ -278,6 +278,30 @@ Response: [ce-2893]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2893 +## Get a trace file + +Get a trace of a specific build of a project + +``` +GET /projects/:id/builds/:build_id/trace +``` + +| Attribute | Type | Required | Description | +|------------|---------|----------|---------------------| +| id | integer | yes | The ID of a project | +| build_id | integer | yes | The ID of a build | + +``` +curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/trace" +``` + +Response: + +| Status | Description | +|-----------|-----------------------------------| +| 200 | Serves the trace file | +| 404 | Build not found or no trace file | + ## Cancel a build Cancel a single build of a project diff --git a/doc/api/groups.md b/doc/api/groups.md index 2821bc21b81..1ccb9715e96 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -265,7 +265,6 @@ GET /groups/:id/members {
"id": 1,
"username": "raymond_smith",
- "email": "ray@smith.org",
"name": "Raymond Smith",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
@@ -274,7 +273,6 @@ GET /groups/:id/members {
"id": 2,
"username": "john_doe",
- "email": "joh@doe.org",
"name": "John Doe",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
diff --git a/doc/api/issues.md b/doc/api/issues.md index 3e78149f442..fc7a7ae0c0c 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -77,7 +77,8 @@ Example response: "created_at" : "2016-01-04T15:31:51.081Z", "iid" : 6, "labels" : [], - "subscribed" : false + "subscribed" : false, + "user_notes_count": 1 } ] ``` @@ -154,7 +155,8 @@ Example response: "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "updated_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z", - "subscribed" : false + "subscribed" : false, + "user_notes_count": 1 } ] ``` @@ -216,7 +218,8 @@ Example response: "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "updated_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z", - "subscribed": false + "subscribed": false, + "user_notes_count": 1 } ``` @@ -271,7 +274,8 @@ Example response: "description" : null, "updated_at" : "2016-01-07T12:44:33.959Z", "milestone" : null, - "subscribed" : true + "subscribed" : true, + "user_notes_count": 0 } ``` @@ -329,7 +333,8 @@ Example response: "id" : 85, "assignee" : null, "milestone" : null, - "subscribed" : true + "subscribed" : true, + "user_notes_count": 0 } ``` diff --git a/doc/api/labels.md b/doc/api/labels.md index 3730c07c5a7..a181c0f57a2 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -39,7 +39,7 @@ Example response: { "name" : "critical", "color" : "#d9534f", - "description": "Criticalissue. Need fix ASAP", + "description": "Critical issue. Need fix ASAP", "open_issues_count": 1, "closed_issues_count": 3, "open_merge_requests_count": 1 @@ -165,3 +165,73 @@ Example response: "description": "Documentation" } ``` + +## Subscribe to a label + +Subscribes the authenticated user to a label to receive notifications. If the +operation is successful, status code `201` together with the updated label is +returned. If the user is already subscribed to the label, the status code `304` +is returned. If the project or label is not found, status code `404` is +returned. + +``` +POST /projects/:id/labels/:label_id/subscription +``` + +| Attribute | Type | Required | Description | +| ---------- | ----------------- | -------- | ------------------------------------ | +| `id` | integer | yes | The ID of a project | +| `label_id` | integer or string | yes | The ID or title of a project's label | + +```bash +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription +``` + +Example response: + +```json +{ + "name": "Docs", + "color": "#cc0033", + "description": "", + "open_issues_count": 0, + "closed_issues_count": 0, + "open_merge_requests_count": 0, + "subscribed": true +} +``` + +## Unsubscribe from a label + +Unsubscribes the authenticated user from a label to not receive notifications +from it. If the operation is successful, status code `200` together with the +updated label is returned. If the user is not subscribed to the label, the +status code `304` is returned. If the project or label is not found, status code +`404` is returned. + +``` +DELETE /projects/:id/labels/:label_id/subscription +``` + +| Attribute | Type | Required | Description | +| ---------- | ----------------- | -------- | ------------------------------------ | +| `id` | integer | yes | The ID of a project | +| `label_id` | integer or string | yes | The ID or title of a project's label | + +```bash +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription +``` + +Example response: + +```json +{ + "name": "Docs", + "color": "#cc0033", + "description": "", + "open_issues_count": 0, + "closed_issues_count": 0, + "open_merge_requests_count": 0, + "subscribed": false +} +``` diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 2057f9d77aa..2930f615fc1 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -67,7 +67,8 @@ Parameters: }, "merge_when_build_succeeds": true, "merge_status": "can_be_merged", - "subscribed" : false + "subscribed" : false, + "user_notes_count": 1 } ] ``` @@ -130,7 +131,8 @@ Parameters: }, "merge_when_build_succeeds": true, "merge_status": "can_be_merged", - "subscribed" : true + "subscribed" : true, + "user_notes_count": 1 } ``` @@ -230,6 +232,7 @@ Parameters: "merge_when_build_succeeds": true, "merge_status": "can_be_merged", "subscribed" : true, + "user_notes_count": 1, "changes": [ { "old_path": "VERSION", @@ -308,7 +311,8 @@ Parameters: }, "merge_when_build_succeeds": true, "merge_status": "can_be_merged", - "subscribed" : true + "subscribed" : true, + "user_notes_count": 0 } ``` @@ -378,7 +382,8 @@ Parameters: }, "merge_when_build_succeeds": true, "merge_status": "can_be_merged", - "subscribed" : true + "subscribed" : true, + "user_notes_count": 1 } ``` @@ -408,11 +413,13 @@ curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.c Merge changes submitted with MR using this API. -If merge success you get `200 OK`. +If the merge succeeds you'll get a `200 OK`. + +If it has some conflicts and can not be merged - you'll get a 405 and the error message 'Branch cannot be merged' -If it has some conflicts and can not be merged - you get 405 and error message 'Branch cannot be merged' +If merge request is already merged or closed - you'll get a 406 and the error message 'Method Not Allowed' -If merge request is already merged or closed - you get 405 and error message 'Method Not Allowed' +If the `sha` parameter is passed and does not match the HEAD of the source - you'll get a 409 and the error message 'SHA does not match HEAD of source branch' If you don't have permissions to accept this merge request - you'll get a 401 @@ -426,7 +433,8 @@ Parameters: - `merge_request_id` (required) - ID of MR - `merge_commit_message` (optional) - Custom merge commit message - `should_remove_source_branch` (optional) - if `true` removes the source branch -- `merged_when_build_succeeds` (optional) - if `true` the MR is merge when the build succeeds +- `merged_when_build_succeeds` (optional) - if `true` the MR is merged when the build succeeds +- `sha` (optional) - if present, then this SHA must match the HEAD of the source branch, otherwise the merge will fail ```json { @@ -472,7 +480,8 @@ Parameters: }, "merge_when_build_succeeds": true, "merge_status": "can_be_merged", - "subscribed" : true + "subscribed" : true, + "user_notes_count": 1 } ``` @@ -537,7 +546,8 @@ Parameters: }, "merge_when_build_succeeds": true, "merge_status": "can_be_merged", - "subscribed" : true + "subscribed" : true, + "user_notes_count": 1 } ``` @@ -562,7 +572,7 @@ GET /projects/:id/merge_requests/:merge_request_id/closes_issues curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/76/merge_requests/1/closes_issues ``` -Example response: +Example response when the GitLab issue tracker is used: ```json [ @@ -602,11 +612,23 @@ Example response: "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.", "created_at" : "2016-01-04T15:31:51.081Z", "iid" : 6, - "labels" : [] + "labels" : [], + "user_notes_count": 1 }, ] ``` +Example response when an external issue tracker (e.g. JIRA) is used: + +```json +[ + { + "id" : "PROJECT-123", + "title" : "Title of this issue" + } +] +``` + ## Subscribe to a merge request Subscribes the authenticated user to a merge request to receive notification. If diff --git a/doc/api/notes.md b/doc/api/notes.md index a6b5b1787fd..7aa1c2155bf 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -15,7 +15,7 @@ GET /projects/:id/issues/:issue_id/notes Parameters: - `id` (required) - The ID of a project -- `issue_id` (required) - The IID of an issue (not ID) +- `issue_id` (required) - The ID of an issue ```json [ @@ -73,7 +73,7 @@ GET /projects/:id/issues/:issue_id/notes/:note_id Parameters: - `id` (required) - The ID of a project -- `issue_id` (required) - The IID of a project issue (not ID) +- `issue_id` (required) - The ID of a project issue - `note_id` (required) - The ID of an issue note ### Create new issue note @@ -87,7 +87,7 @@ POST /projects/:id/issues/:issue_id/notes Parameters: - `id` (required) - The ID of a project -- `issue_id` (required) - The IID of an issue (not ID) +- `issue_id` (required) - The ID of an issue - `body` (required) - The content of a note - `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z @@ -102,7 +102,7 @@ PUT /projects/:id/issues/:issue_id/notes/:note_id Parameters: - `id` (required) - The ID of a project -- `issue_id` (required) - The IID of an issue (not ID) +- `issue_id` (required) - The ID of an issue - `note_id` (required) - The ID of a note - `body` (required) - The content of a note @@ -120,7 +120,7 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The IID of an issue | +| `issue_id` | integer | yes | The ID of an issue | | `note_id` | integer | yes | The ID of a note | ```bash diff --git a/doc/api/projects.md b/doc/api/projects.md index de1faadebf5..f5f195b97df 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -424,6 +424,7 @@ Parameters: - `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) +- `container_registry_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `import_url` (optional) @@ -447,6 +448,7 @@ Parameters: - `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) +- `container_registry_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `import_url` (optional) @@ -472,6 +474,7 @@ Parameters: - `builds_enabled` (optional) - `wiki_enabled` (optional) - `snippets_enabled` (optional) +- `container_registry_enabled` (optional) - `public` (optional) - if `true` same as setting visibility_level = 20 - `visibility_level` (optional) - `public_builds` (optional) diff --git a/doc/api/runners.md b/doc/api/runners.md index cc6c6b7cb2f..ddfa298f79d 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -275,7 +275,7 @@ POST /projects/:id/runners | `runner_id` | integer | yes | The ID of a runner | ``` -curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/project/9/runners" -F "runner_id=9" +curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" -F "runner_id=9" ``` Example response: @@ -306,7 +306,7 @@ DELETE /projects/:id/runners/:runner_id | `runner_id` | integer | yes | The ID of a runner | ``` -curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/project/9/runners/9" +curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9" ``` Example response: diff --git a/doc/api/services.md b/doc/api/services.md index 7d45b2cf463..ccfc0fccb7f 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -16,8 +16,8 @@ PUT /projects/:id/services/asana Parameters: -- `api_key` (**required**) - User API token. User must have access to task,all comments will be attributed to this user. -- `restrict_to_branch` (optional) - Comma-separated list of branches which will beautomatically inspected. Leave blank to include all branches. +- `api_key` (**required**) - User API token. User must have access to task, all comments will be attributed to this user. +- `restrict_to_branch` (optional) - Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches. ### Delete Asana service @@ -491,7 +491,7 @@ Jira issue tracker Set JIRA service for a project. -> Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to easily navigate to the Jira issue tracker. See the [integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) for details. Support for referencing commits and automatic closing of Jira issues directly from GitLab is [available in GitLab EE.](http://doc.gitlab.com/ee/integration/jira.html) +> Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to easily navigate to the Jira issue tracker. See the [integration doc](http://docs.gitlab.com/ce/integration/external-issue-tracker.html) for details. Support for referencing commits and automatic closing of Jira issues directly from GitLab is [available in GitLab EE.](http://docs.gitlab.com/ee/integration/jira.html) ``` PUT /projects/:id/services/jira @@ -503,6 +503,8 @@ Parameters: - `project_url` (**required**) - Project url - `issues_url` (**required**) - Issue url - `description` (optional) - Jira issue tracker +- `username` (optional) - Jira username +- `password` (optional) - Jira password ### Delete JIRA service diff --git a/doc/api/settings.md b/doc/api/settings.md index 1e745115dc8..43a0fe35e42 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -37,7 +37,8 @@ Example response: "created_at" : "2016-01-04T15:44:55.176Z", "default_project_visibility" : 0, "gravatar_enabled" : true, - "sign_in_text" : null + "sign_in_text" : null, + "container_registry_token_expire_delay": 5 } ``` @@ -64,6 +65,7 @@ PUT /application/settings | `restricted_signup_domains` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. | | `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider | | `after_sign_out_path` | string | no | Where to redirect users after logout | +| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes | ```bash curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1 @@ -90,6 +92,7 @@ Example response: "default_snippet_visibility": 0, "restricted_signup_domains": [], "user_oauth_applications": true, - "after_sign_out_path": "" + "after_sign_out_path": "", + "container_registry_token_expire_delay": 5 } ``` diff --git a/doc/ci/deployment/README.md b/doc/ci/examples/deployment/README.md index 7d91ce6710f..7d91ce6710f 100644 --- a/doc/ci/deployment/README.md +++ b/doc/ci/examples/deployment/README.md diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 6a42a935abd..386b8e29fcf 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -212,8 +212,8 @@ If you want to receive e-mail notifications about the result status of the builds, you should explicitly enable the **Builds Emails** service under your project's settings. -For more information read the [Builds emails service documentation] -(../../project_services/builds_emails.md). +For more information read the +[Builds emails service documentation](../../project_services/builds_emails.md). ## Builds badge diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index a06650b3387..b42d7a62ebc 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -125,7 +125,13 @@ shared runners will only run the jobs they are equipped to run. For instance, at GitLab we have runners tagged with "rails" if they contain the appropriate dependencies to run Rails test suites. -### Be Careful with Sensitive Information +### Prevent runner with tags from picking jobs without tags + +You can configure a runner to prevent it from picking jobs with tags when +the runnner does not have tags assigned. This setting is available on each +runner in *Project Settings* > *Runners*. + +### Be careful with sensitive information If you can run a build on a runner, you can get access to any code it runs and get the token of the runner. With shared runners, this means that anyone diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index 79ed512aabb..5c316510d0e 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -33,7 +33,7 @@ POST /projects/:id/trigger/builds The required parameters are the trigger's `token` and the Git `ref` on which the trigger will be performed. Valid refs are the branch, the tag or the commit -SHA. The `:id` of a project can be found by [querying the API](../api/projects.md) +SHA. The `:id` of a project can be found by [querying the API](../../api/projects.md) or by visiting the **Triggers** page which provides self-explanatory examples. When a rebuild is triggered, the information is exposed in GitLab's UI under diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 7e9bced7616..a3481f58c6c 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -128,7 +128,7 @@ builds, including deploy builds. This can be an array or a multi-line string. ### after_script >**Note:** -Introduced in GitLab 8.7 and GitLab Runner v1.2. +Introduced in GitLab 8.7 and requires Gitlab Runner v1.2 (not yet released) `after_script` is used to define the command that will be run after for all builds. This has to be an array or a multi-line string. @@ -348,7 +348,7 @@ job_name: | allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status | | when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` | | dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them| -| artifacts | no | Define list build artifacts | +| artifacts | no | Define list of build artifacts | | cache | no | Define list of files that should be cached between subsequent runs | | before_script | no | Override a set of commands that are executed before build | | after_script | no | Override a set of commands that are executed after build | diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md new file mode 100644 index 00000000000..4df24ef13cc --- /dev/null +++ b/doc/container_registry/README.md @@ -0,0 +1,113 @@ +# GitLab Container Registry + +> **Note:** +This feature was [introduced][ce-4040] in GitLab 8.8. + +> **Note:** +This document is about the user guide. To learn how to enable GitLab Container +Registry across your GitLab instance, visit the +[administrator documentation](../administration/container_registry.md). + +With the Docker Container Registry integrated into GitLab, every project can +have its own space to store its Docker images. + +You can read more about Docker Registry at https://docs.docker.com/registry/introduction/. + +--- + +## Enable the Container Registry for your project + +1. First, ask your system administrator to enable GitLab Container Registry + following the [administration documentation](../administration/container_registry.md). + If you are using GitLab.com, this is enabled by default so you can start using + the Registry immediately. + +1. Go to your project's settings and enable the **Container Registry** feature + on your project. For new projects this might be enabled by default. For + existing projects you will have to explicitly enable it. + + ![Enable Container Registry](img/project_feature.png) + +## Build and push images + +After you save your project's settings, you should see a new link in the +sidebar called **Container Registry**. Following this link will get you to +your project's Registry panel where you can see how to login to the Container +Registry using your GitLab credentials. + +For example if the Registry's URL is `registry.example.com`, the you should be +able to login with: + +``` +docker login registry.example.com +``` + +Building and publishing images should be a straightforward process. Just make +sure that you are using the Registry URL with the namespace and project name +that is hosted on GitLab: + +``` +docker build -t registry.example.com/group/project . +docker push registry.example.com/group/project +``` + +## Use images from GitLab Container Registry + +To download and run a container from images hosted in GitLab Container Registry, +use `docker run`: + +``` +docker run [options] registry.example.com/group/project [arguments] +``` + +For more information on running Docker containers, visit the +[Docker documentation][docker-docs]. + +## Control Container Registry from within GitLab + +GitLab offers a simple Container Registry management panel. Go to your project +and click **Container Registry** in the left sidebar. + +This view will show you all tags in your project and will easily allow you to +delete them. + +![Container Registry panel](img/container_registry.png) + +## Build and push images using GitLab CI + +> **Note:** +This feature requires GitLab 8.8 and GitLab Runner 1.2. + +Make sure that your GitLab Runner is configured to allow building docker images. +You have to check the [Using Docker Build documentation](../../ci/docker/using_docker_build.md). + +You can use [docker:dind](https://hub.docker.com/_/docker/) to build your images, +and this is how `.gitlab-ci.yml` should look like: + +``` + build_image: + image: docker:git + services: + - docker:dind + stage: build + script: + - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com + - docker build -t registry.example.com/group/project:latest . + - docker push registry.example.com/group/project:latest +``` + +You have to use the credentials of the special `gitlab-ci-token` user with its +password stored in `$CI_BUILD_TOKEN` in order to push to the Registry connected +to your project. This allows you to automated building and deployment of your +Docker images. + +## Limitations + +In order to use a container image from your private project as an `image:` in +your `.gitlab-ci.yml`, you have to follow the +[Using a private Docker Registry][private-docker] +documentation. This workflow will be simplified in the future. + +[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 +[docker-docs]: https://docs.docker.com/engine/userguide/intro/ +[private-docker]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry diff --git a/doc/container_registry/img/container_registry.png b/doc/container_registry/img/container_registry.png Binary files differnew file mode 100644 index 00000000000..e9505a73b40 --- /dev/null +++ b/doc/container_registry/img/container_registry.png diff --git a/doc/container_registry/img/project_feature.png b/doc/container_registry/img/project_feature.png Binary files differnew file mode 100644 index 00000000000..57a73d253c0 --- /dev/null +++ b/doc/container_registry/img/project_feature.png diff --git a/doc/development/README.md b/doc/development/README.md index aa7d54c01d0..c5d5af43864 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -7,6 +7,7 @@ - [Gotchas](gotchas.md) to avoid - [How to dump production data to staging](db_dump.md) - [Instrumentation](instrumentation.md) +- [Licensing](licensing.md) for ensuring license compliance - [Migration Style Guide](migration_style_guide.md) for creating safe migrations - [Performance guidelines](performance.md) - [Rake tasks](rake_tasks.md) for development diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 187ec9e7b75..8292b393757 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -127,7 +127,7 @@ Inside the document: ``` If the document you are editing resides in a place other than the GitLab CE/EE `doc/` directory, instead of the relative link, use the full path: - `http://doc.gitlab.com/ce/administration/restart_gitlab.html`. + `http://docs.gitlab.com/ce/administration/restart_gitlab.html`. Replace `reconfigure` with `restart` where appropriate. ## Installation guide @@ -266,5 +266,5 @@ curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -d "restricted_signup_domai [cURL]: http://curl.haxx.se/ "cURL website" [single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html -[gfm]: http://doc.gitlab.com/ce/markdown/markdown.html#newlines "GitLab flavored markdown documentation" +[gfm]: http://docs.gitlab.com/ce/markdown/markdown.html#newlines "GitLab flavored markdown documentation" [doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation" diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md index c1cf2e77c26..9168c70945a 100644 --- a/doc/development/instrumentation.md +++ b/doc/development/instrumentation.md @@ -1,12 +1,125 @@ # Instrumenting Ruby Code -GitLab Performance Monitoring allows instrumenting of custom blocks of Ruby -code. This can be used to measure the time spent in a specific part of a larger -chunk of code. The resulting data is stored as a field in the transaction that -executed the block. +GitLab Performance Monitoring allows instrumenting of both methods and custom +blocks of Ruby code. Method instrumentation is the primary form of +instrumentation with block-based instrumentation only being used when we want to +drill down to specific regions of code within a method. -To start measuring a block of Ruby code you should use `Gitlab::Metrics.measure` -and give it a name: +## Instrumenting Methods + +Instrumenting methods is done by using the `Gitlab::Metrics::Instrumentation` +module. This module offers a few different methods that can be used to +instrument code: + +* `instrument_method`: instruments a single class method. +* `instrument_instance_method`: instruments a single instance method. +* `instrument_class_hierarchy`: given a Class this method will recursively + instrument all sub-classes (both class and instance methods). +* `instrument_methods`: instruments all public class methods of a Module. +* `instrument_instance_methods`: instruments all public instance methods of a + Module. + +To remove the need for typing the full `Gitlab::Metrics::Instrumentation` +namespace you can use the `configure` class method. This method simply yields +the supplied block while passing `Gitlab::Metrics::Instrumentation` as its +argument. An example: + +``` +Gitlab::Metrics::Instrumentation.configure do |conf| + conf.instrument_method(Foo, :bar) + conf.instrument_method(Foo, :baz) +end +``` + +Using this method is in general preferred over directly calling the various +instrumentation methods. + +Method instrumentation should be added in the initializer +`config/initializers/metrics.rb`. + +### Examples + +Instrumenting a single method: + +``` +Gitlab::Metrics::Instrumentation.configure do |conf| + conf.instrument_method(User, :find_by) +end +``` + +Instrumenting an entire class hierarchy: + +``` +Gitlab::Metrics::Instrumentation.configure do |conf| + conf.instrument_class_hierarchy(ActiveRecord::Base) +end +``` + +Instrumenting all public class methods: + +``` +Gitlab::Metrics::Instrumentation.configure do |conf| + conf.instrument_methods(User) +end +``` + +### Checking Instrumented Methods + +The easiest way to check if a method has been instrumented is to check its +source location. For example: + +``` +method = Rugged::TagCollection.instance_method(:[]) + +method.source_location +``` + +If the source location points to `lib/gitlab/metrics/instrumentation.rb` you +know the method has been instrumented. + +If you're using Pry you can use the `$` command to display the source code of a +method (along with its source location), this is easier than running the above +Ruby code. In case of the above snippet you'd run the following: + +``` +$ Rugged::TagCollection#[] +``` + +This will print out something along the lines of: + +``` +From: /path/to/your/gitlab/lib/gitlab/metrics/instrumentation.rb @ line 148: +Owner: #<Module:0x0055f0865c6d50> +Visibility: public +Number of lines: 21 + +def #{name}(#{args_signature}) + trans = Gitlab::Metrics::Instrumentation.transaction + + if trans + start = Time.now + retval = super + duration = (Time.now - start) * 1000.0 + + if duration >= Gitlab::Metrics.method_call_threshold + trans.increment(:method_duration, duration) + + trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES, + { duration: duration }, + method: #{label.inspect}) + end + + retval + else + super + end +end +``` + +## Instrumenting Ruby Blocks + +Measuring blocks of Ruby code is done by calling `Gitlab::Metrics.measure` and +passing it a block. For example: ```ruby Gitlab::Metrics.measure(:foo) do @@ -14,6 +127,10 @@ Gitlab::Metrics.measure(:foo) do end ``` +The block is executed and the execution time is stored as a set of fields in the +currently running transaction. If no transaction is present the block is yielded +without measuring anything. + 3 values are measured for a block: 1. The real time elapsed, stored in NAME_real_time. diff --git a/doc/development/licensing.md b/doc/development/licensing.md new file mode 100644 index 00000000000..8c8c7486fff --- /dev/null +++ b/doc/development/licensing.md @@ -0,0 +1,93 @@ +# GitLab Licensing and Compatibility + +GitLab CE is licensed under the terms of the MIT License. GitLab EE is licensed under "The GitLab Enterprise Edition (EE) license" wherein there are more restrictions. See their respective LICENSE files ([CE][CE], [EE][EE]) for more information. + +## Automated Testing + +In order to comply with the terms the libraries we use are licensed under, we have to make sure to check new gems for compatible licenses whenever they're added. To automate this process, we use the [license_finder][license_finder] gem by Pivotal. It runs every time a new commit is pushed and verifies that all gems in the bundle use a license that doesn't conflict with the licensing of either GitLab Community Edition or GitLab Enterprise Edition. + +There are some limitations with the automated testing, however. CSS and JavaScript libraries, as well as any Ruby libraries not included by way of Bundler, must be verified manually and independently. Take care whenever one such library is used, as automated tests won't catch problematic licenses from them. + +Some gems may not include their license information in their `gemspec` file. These won't be detected by License Finder, and will have to be verified manually. + +### License Finder commands + +There are a few basic commands License Finder provides that you'll need in order to manage license detection. + +To verify that the checks are passing, and/or to see what dependencies are causing the checks to fail: + +``` +bundle exec license_finder +``` + +To whitelist a new license: + +``` +license_finder whitelist add MIT +``` + +To blacklist a new license: + +``` +license_finder blacklist add GPLv2 +``` + +To tell License Finder about a dependency's license if it isn't auto-detected: + +``` +license_finder licenses add my_unknown_dependency MIT +``` + +For all of the above, please include `--why "Reason"` and `--who "My Name"` so the `decisions.yml` file can keep track of when, why, and who approved of a dependency. + +More detailed information on how the gem and its commands work is available in the [License Finder README][license_finder]. + +## Acceptable Licenses + +Libraries with the following licenses are acceptable for use: + +- [The MIT License][MIT] (the MIT Expat License specifically): The MIT License requires that the license itself is included with all copies of the source. It is a permissive (non-copyleft) license as defined by the Open Source Initiative. +- [LGPL][LGPL] (version 2, version 3): GPL constraints regarding modification and redistribution under the same license are not required of projects using an LGPL library, only upon modification of the LGPL-licensed library itself. +- [Apache 2.0 License][apache-2]: A permissive license that also provides an express grant of patent rights from contributors to users. +- [Ruby 1.8 License][ruby-1.8]: Dual-licensed under either itself or the GPLv2, defer to the Ruby License itself. Acceptable because of point 3b: "You may distribute the software in object code or binary form, provided that you do at least ONE of the following: b) accompany the distribution with the machine-readable source of the software." +- [Ruby 1.9 License][ruby-1.9]: Dual-licensed under either itself or the BSD 2-Clause License, defer to BSD 2-Clause. +- [BSD 2-Clause License][BSD-2-Clause]: A permissive (non-copyleft) license as defined by the Open Source Initiative. +- [BSD 3-Clause License][BSD-3-Clause] (also known as New BSD or Modified BSD): A permissive (non-copyleft) license as defined by the Open Source Initiative +- [ISC License][ISC] (also known as the OpenBSD License): A permissive (non-copyleft) license as defined by the Open Source Initiative. + +## Unacceptable Licenses + +Libraries with the following licenses are unacceptable for use: + +- [GNU GPL][GPL] (version 1, [version 2][GPLv2], [version 3][GPLv3], or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects. +- [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects. + +## Notes + +Decisions regarding the GNU GPL licenses are based on information provided by [The GNU Project][GNU-GPL-FAQ], as well as [the Open Source Initiative][OSI-GPL], which both state that linking GPL libraries makes the program itself GPL. + +If a gem uses a license which is not listed above, open an issue and ask. If a license is not included in the "acceptable" list, operate under the assumption that it is not acceptable. + +Keep in mind that each license has its own restrictions (typically defined in their body text). Please make sure to comply with those restrictions at all times whenever an external library is used. + +Gems which are included only in the "development" or "test" groups by Bundler are exempt from license requirements, as they're not distributed for use in production. + +**NOTE:** This document is **not** legal advice, nor is it comprehensive. It should not be taken as such. + +[CE]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/LICENSE +[EE]: https://gitlab.com/gitlab-org/gitlab-ee/blob/master/LICENSE +[license_finder]: https://github.com/pivotal/LicenseFinder +[MIT]: http://choosealicense.com/licenses/mit/ +[LGPL]: http://choosealicense.com/licenses/lgpl-3.0/ +[apache-2]: http://choosealicense.com/licenses/apache-2.0/ +[ruby-1.8]: https://github.com/ruby/ruby/blob/ruby_1_8_6/COPYING +[ruby-1.9]: https://www.ruby-lang.org/en/about/license.txt +[BSD-2-Clause]: https://opensource.org/licenses/BSD-2-Clause +[BSD-3-Clause]: https://opensource.org/licenses/BSD-3-Clause +[ISC]: https://opensource.org/licenses/ISC +[GPL]: http://choosealicense.com/licenses/gpl-3.0/ +[GPLv2]: http://www.gnu.org/licenses/gpl-2.0.txt +[GPLv3]: http://www.gnu.org/licenses/gpl-3.0.txt +[AGPLv3]: http://choosealicense.com/licenses/agpl-3.0/ +[GNU-GPL-FAQ]: http://www.gnu.org/licenses/gpl-faq.html#IfLibraryIsGPL +[OSI-GPL]: https://opensource.org/faq#linking-proprietary-code diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 28dedf3978c..02e024ca15a 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -8,7 +8,10 @@ In addition, having to take a server offline for a an upgrade small or big is a big burden for most organizations. For this reason it is important that your migrations are written carefully, can be applied online and adhere to the style guide below. -It's advised to have offline migrations only in major GitLab releases. +Migrations should not require GitLab installations to be taken offline unless +_absolutely_ necessary. If a migration requires downtime this should be +clearly mentioned during the review process as well as being documented in the +monthly release post. When writing your migrations, also consider that databases might have stale data or inconsistencies and guard for that. Try to make as little assumptions as possible @@ -58,6 +61,45 @@ remove_index :namespaces, column: :name if index_exists?(:namespaces, :name) If you need to add an unique index please keep in mind there is possibility of existing duplicates. If it is possible write a separate migration for handling this situation. It can be just removing or removing with overwriting all references to these duplicates depend on situation. +When adding an index make sure to use the method `add_concurrent_index` instead +of the regular `add_index` method. The `add_concurrent_index` method +automatically creates concurrent indexes when using PostgreSQL, removing the +need for downtime. To use this method you must disable transactions by calling +the method `disable_ddl_transaction!` in the body of your migration class like +so: + +``` +class MyMigration < ActiveRecord::Migration + disable_ddl_transaction! + + def change + + end +end +``` + +## Adding Columns With Default Values + +When adding columns with default values you should use the method +`add_column_with_default`. This method ensures the table is updated without +requiring downtime. This method is not reversible so you must manually define +the `up` and `down` methods in your migration class. + +For example, to add the column `foo` to the `projects` table with a default +value of `10` you'd write the following: + +``` +class MyMigration < ActiveRecord::Migration + def up + add_column_with_default(:projects, :foo, :integer, 10) + end + + def down + remove_column(:projects, :foo) + end +end +``` + ## Testing Make sure that your migration works with MySQL and PostgreSQL with data. An empty database does not guarantee that your migration is correct. @@ -74,7 +116,7 @@ Example with Arel: users = Arel::Table.new(:users) users.group(users[:user_id]).having(users[:id].count.gt(5)) -#updtae other tables with this results +#update other tables with these results ``` Example with plain SQL and `quote_string` helper: @@ -89,4 +131,4 @@ select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(i execute("UPDATE taggings SET tag_id = #{origin_tag_id} WHERE tag_id IN(#{duplicate_ids.join(",")})") execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})") end -```
\ No newline at end of file +``` diff --git a/doc/development/testing.md b/doc/development/testing.md index 33eed29ba5c..513457d203a 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -65,7 +65,7 @@ the command line via `bundle exec teaspoon`, or via a web browser at - Use `context` to test branching logic. - Don't `describe` symbols (see [Gotchas](gotchas.md#dont-describe-symbols)). - Don't supply the `:each` argument to hooks since it's the default. -- Prefer `not_to` to `to_not`. +- Prefer `not_to` to `to_not` (_this is enforced by Rubocop_). - Try to match the ordering of tests to the ordering within the class. - Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines to separate phases. diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md index a3e260a5f89..5893b7c219e 100644 --- a/doc/development/ui_guide.md +++ b/doc/development/ui_guide.md @@ -6,3 +6,51 @@ We created a page inside GitLab where you can check commonly used html and css e When you run GitLab instance locally - just visit http://localhost:3000/help/ui page to see UI examples you can use during GitLab development. + +## Design repository + +All design files are stored in the [gitlab-design](https://gitlab.com/gitlab-org/gitlab-design) +repository and maintained by GitLab UX designers. + +## Navigation + +GitLab's layout contains 2 sections: the left sidebar and the content. The left sidebar contains a static navigation menu. +This menu will be visible regardless of what page you visit. The left sidebar also contains the GitLab logo +and the current user's profile picture. The content section contains a header and the content itself. +The header describes the current GitLab page and what navigation is +available to user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example when user visits one of the +project pages the header will contain a project name and navigation for that project. When the user visits a group page it will contain a group name and navigation related to this group. + +### Adding new tab to header navigation + +We try to keep the amount of tabs in the header navigation between 5 and 10 so that it fits on a typical laptop screen. We also try not to confuse the user with too many options. Ideally each +tab should represent separate functionality. Everything related to the issue +tracker should be under the 'Issues' tab while everything related to the wiki should +be under 'Wiki' tab and so on and so forth. + +## Mobile screen size + +We want GitLab to work well on small mobile screens as well. Size limitations make it is impossible to fit everything on a mobile screen. In this case it is OK to hide +part of the UI for smaller resolutions in favor of a better user experience. +However core functionality like browsing files, creating issues, writing comments, should +be available on all resolutions. + +## Icons + +* `trash` icon for button or link that does destructive action like removing +information from database or file system +* `x` icon for closing/hiding UI element. For example close modal window +* `pencil` icon for edit button or link +* `eye` icon for subscribe action +* `rss` for rss/atom feed +* `plus` for link or dropdown that lead to page where you create new object (For example new issue page) + + +## Buttons + +* Button should contain icon or text. Exceptions should be approved by UX designer. +* Use red button for destructive actions (not revertable). For example removing issue. +* Use green or blue button for primary action. Primary button should be only one. +Do not use both green and blue button in one form. +* For all other cases use default white button + diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md index 87f078def04..5221d85b661 100644 --- a/doc/gitlab-basics/create-issue.md +++ b/doc/gitlab-basics/create-issue.md @@ -24,4 +24,4 @@ You may assign the Issue to a user, add a milestone and add labels (they are all ![Submit new issue](basicsimages/submit_new_issue.png) -Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://doc.gitlab.com/ce/customization/issue_closing.html). +Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://docs.gitlab.com/ce/customization/issue_closing.html). diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md index b545d62549d..f737dffc024 100644 --- a/doc/gitlab-basics/create-project.md +++ b/doc/gitlab-basics/create-project.md @@ -14,7 +14,7 @@ Fill out the required information: 1. Select a [visibility level](https://gitlab.com/help/public_access/public_access) -1. You can also [import your existing projects](http://doc.gitlab.com/ce/workflow/importing/README.html) +1. You can also [import your existing projects](http://docs.gitlab.com/ce/workflow/importing/README.html) 1. Click on "create project" diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md index dcdf49d3379..820934f97f1 100644 --- a/doc/hooks/custom_hooks.md +++ b/doc/hooks/custom_hooks.md @@ -2,7 +2,7 @@ **Note: Custom git hooks must be configured on the filesystem of the GitLab server. Only GitLab server administrators will be able to complete these tasks. -Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** +Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://docs.gitlab.com/ee/git_hooks/git_hooks.html).** Git natively supports hooks that are executed on different actions. Examples of server-side git hooks include pre-receive, post-receive, and update. diff --git a/doc/install/installation.md b/doc/install/installation.md index e3af3022262..d9290b1fa76 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -6,7 +6,7 @@ Since an installation from source is a lot of work and error prone we strongly r One reason the Omnibus package is more reliable is its use of Runit to restart any of the GitLab processes in case one crashes. On heavily used GitLab instances the memory usage of the Sidekiq background worker will grow over time. -Omnibus packages solve this by [letting the Sidekiq terminate gracefully](http://doc.gitlab.com/ce/operations/sidekiq_memory_killer.html) if it uses too much memory. +Omnibus packages solve this by [letting the Sidekiq terminate gracefully](http://docs.gitlab.com/ce/operations/sidekiq_memory_killer.html) if it uses too much memory. After this termination Runit will detect Sidekiq is not running and will start it. Since installations from source don't have Runit, Sidekiq can't be terminated and its memory usage will grow over time. @@ -269,9 +269,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-7-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-9-stable gitlab -**Note:** You can change `8-7-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-9-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -394,7 +394,7 @@ GitLab Shell is an SSH access and repository management software developed speci cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git cd gitlab-workhorse - sudo -u git -H git checkout v0.7.1 + sudo -u git -H git checkout v0.7.5 sudo -u git -H make ### Initialize Database and Activate Advanced Features diff --git a/doc/install/relative_url.md b/doc/install/relative_url.md index 0245febfcd8..44d2a14f366 100644 --- a/doc/install/relative_url.md +++ b/doc/install/relative_url.md @@ -132,5 +132,5 @@ To disable the relative URL: 1. Follow the same as above starting from 2. and set up the GitLab URL to one that doesn't contain a relative path. -[omnibus-rel]: http://doc.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab "How to setup relative URL in Omnibus GitLab" +[omnibus-rel]: http://docs.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab "How to setup relative URL in Omnibus GitLab" [restart gitlab]: ../administration/restart_gitlab.md#installations-from-source "How to restart GitLab" diff --git a/doc/install/requirements.md b/doc/install/requirements.md index df8e8bdc476..09c6211b3ab 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -81,7 +81,7 @@ errors during usage. - More users? Run it on [multiple application servers](https://about.gitlab.com/high-availability/) We recommend having at least 1GB of swap on your server, even if you currently have -enough available RAM. Having swap will help reduce the chance of errors occuring +enough available RAM. Having swap will help reduce the chance of errors occurring if your available memory changes. Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those. @@ -150,3 +150,4 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o - Safari 7+ (known problem: required fields in html5 do not work) - Opera (Latest released version) - Internet Explorer (IE) 11+ but please make sure that you have the `Compatibility View` mode disabled. +- Edge (Latest stable version) diff --git a/doc/integration/README.md b/doc/integration/README.md index 6fe04aa2a06..fd330dd7a7d 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -19,7 +19,7 @@ See the documentation below for details on how to configure these services. GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. -[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html +[jenkins]: http://docs.gitlab.com/ee/integration/jenkins.html ## Project services diff --git a/doc/integration/cas.md b/doc/integration/cas.md index e6b2071f193..e34e306f9ac 100644 --- a/doc/integration/cas.md +++ b/doc/integration/cas.md @@ -27,17 +27,18 @@ To enable the CAS OmniAuth provider you must register your application with your ```ruby gitlab_rails['omniauth_providers'] = [ { - name: "cas3", - label: "cas", - args: { - url: 'CAS_SERVER', - login_url: '/CAS_PATH/login', - service_validate_url: '/CAS_PATH/p3/serviceValidate', - logout_url: '/CAS_PATH/logout'} } - } + "name"=> "cas3", + "label"=> "cas", + "args"=> { + "url"=> 'CAS_SERVER', + "login_url"=> '/CAS_PATH/login', + "service_validate_url"=> '/CAS_PATH/p3/serviceValidate', + "logout_url"=> '/CAS_PATH/logout' + } } ] ``` + For installations from source: @@ -57,6 +58,8 @@ To enable the CAS OmniAuth provider you must register your application with your 1. Save the configuration file. +1. Run `gitlab-ctl reconfigure` for the omnibus package. + 1. Restart GitLab for the changes to take effect. On the sign in page there should now be a CAS tab in the sign in form. diff --git a/doc/integration/google.md b/doc/integration/google.md index f9a20dd840d..82978b68a34 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -11,9 +11,9 @@ To enable the Google OAuth2 OmniAuth provider you must register your application - Project ID: Must be unique to all Google Developer registered applications. Google provides a randomly generated Project ID by default. You can use the randomly generated ID or choose a new one. 1. Refresh the page. You should now see your new project in the list. Click on the project. -1. Select "APIs & auth" in the left menu. +1. Select the "Google APIs" tab in the Overview. -1. Select "APIs" in the submenu. +1. Select and enable the following Google APIs - listed under "Popular APIs" - Enable `Contacts API` - Enable `Google+ API` diff --git a/doc/integration/img/enabled-oauth-sign-in-sources.png b/doc/integration/img/enabled-oauth-sign-in-sources.png Binary files differnew file mode 100644 index 00000000000..95f8bbdcd24 --- /dev/null +++ b/doc/integration/img/enabled-oauth-sign-in-sources.png diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index cab329c0dec..820f40f81a9 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -11,6 +11,7 @@ of the configured mechanisms. - [Supported Providers](#supported-providers) - [Enable OmniAuth for an Existing User](#enable-omniauth-for-an-existing-user) - [OmniAuth configuration sample when using Omnibus GitLab](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master#omniauth-google-twitter-github-login) +- [Enable or disable Sign In with an OmniAuth provider without disabling import sources](#enable-or-disable-sign-in-with-an-omniauth-provider-without-disabling-import-sources) ## Supported Providers @@ -191,3 +192,17 @@ experience [in the public Wiki](https://github.com/gitlabhq/gitlab-public-wiki/w While we can't officially support every possible authentication mechanism out there, we'd like to at least help those with specific needs. + +## Enable or disable Sign In with an OmniAuth provider without disabling import sources + +>**Note:** +This setting was introduced with version 8.8 of GitLab. + +Administrators are able to enable or disable Sign In via some OmniAuth providers. + +>**Note:** +By default Sign In is enabled via all the OAuth Providers that have been configured in `config/gitlab.yml`. + +In order to enable/disable an OmniAuth provider, go to Admin Area -> Settings -> Sign-in Restrictions section -> Enabled OAuth Sign-In sources and select the providers you want to enable or disable. + +![Enabled OAuth Sign-In sources](img/enabled-oauth-sign-in-sources.png) diff --git a/doc/intro/README.md b/doc/intro/README.md index ab298d3808e..382d10aaf40 100644 --- a/doc/intro/README.md +++ b/doc/intro/README.md @@ -39,4 +39,4 @@ Install and update your GitLab installation. - [Install GitLab](https://about.gitlab.com/installation/) - [Update GitLab](https://about.gitlab.com/update/) -- [Explore Omnibus GitLab configuration options](http://doc.gitlab.com/omnibus/settings/configuration.html) +- [Explore Omnibus GitLab configuration options](http://docs.gitlab.com/omnibus/settings/configuration.html) diff --git a/doc/logs/logs.md b/doc/logs/logs.md index 27937e51764..f84060b8d07 100644 --- a/doc/logs/logs.md +++ b/doc/logs/logs.md @@ -1,6 +1,6 @@ ## Log system -GitLab has advanced log system so everything is logging and you can analize your instance using various system log files. -In addition to system log files, GitLab Enterprise Edition comes with Audit Events. Find more about them [in Audit Events documentation](http://doc.gitlab.com/ee/administration/audit_events.html) +GitLab has an advanced log system where everything is logged so that you can analyze your instance using various system log files. +In addition to system log files, GitLab Enterprise Edition comes with Audit Events. Find more about them [in Audit Events documentation](http://docs.gitlab.com/ee/administration/audit_events.html) System log files are typically plain text in a standard log file format. This guide talks about how to read and use these system log files. @@ -67,13 +67,13 @@ gitlab-shell is using by Gitlab for executing git commands and provide ssh acces ``` I, [2015-02-13T06:17:00.671315 #9291] INFO -- : Adding project root/example.git at </var/opt/gitlab/git-data/repositories/root/dcdcdcdcd.git>. -I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory and simlinking global hooks directory for /var/opt/gitlab/git-data/repositories/root/example.git. +I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory and symlinking global hooks directory for /var/opt/gitlab/git-data/repositories/root/example.git. ``` #### unicorn_stderr.log This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for omnibus package or in `/home/git/gitlab/log/unicorn_stderr.log` for installations from the source. -Unicorn is a high-performance forking Web server which is used for serving GitLab application. You can look at this log, for example, if your application does not respond. This log cantains all information about state of unicorn processes at any given time. +Unicorn is a high-performance forking Web server which is used for serving the GitLab application. You can look at this log if, for example, your application does not respond. This log contains all information about the state of unicorn processes at any given time. ``` I, [2015-02-13T06:14:46.680381 #9047] INFO -- : Refreshing Gem list diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 4f199b6af6f..236eb7b12c4 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -8,6 +8,7 @@ * [Multiple underscores in words](#multiple-underscores-in-words) * [URL auto-linking](#url-auto-linking) * [Code and Syntax Highlighting](#code-and-syntax-highlighting) +* [Inline Diff](#inline-diff) * [Emoji](#emoji) * [Special GitLab references](#special-gitlab-references) * [Task lists](#task-lists) @@ -153,6 +154,19 @@ s = "There is no highlighting for this." But let's throw in a <b>tag</b>. ``` +## Inline Diff + +With inline diffs tags you can display {+ additions +} or [- deletions -]. + +The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}. + +However the wrapping tags cannot be mixed as such: + +- {+ additions +] +- [+ additions +} +- {- deletions -] +- [- deletions -} + ## Emoji Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: @@ -185,20 +199,23 @@ GFM will turn that reference into a link so you can navigate between them easily GFM will recognize the following: -| input | references | -|:-----------------------|:---------------------------| -| `@user_name` | specific user | -| `@group_name` | specific group | -| `@all` | entire team | -| `#123` | issue | -| `!123` | merge request | -| `$123` | snippet | -| `~123` | label by ID | -| `~bug` | one-word label by name | -| `~"feature request"` | multi-word label by name | -| `9ba12248` | specific commit | -| `9ba12248...b19a04f5` | commit range comparison | -| `[README](doc/README)` | repository file references | +| input | references | +|:-----------------------|:--------------------------- | +| `@user_name` | specific user | +| `@group_name` | specific group | +| `@all` | entire team | +| `#123` | issue | +| `!123` | merge request | +| `$123` | snippet | +| `~123` | label by ID | +| `~bug` | one-word label by name | +| `~"feature request"` | multi-word label by name | +| `%123` | milestone by ID | +| `%v1.23` | one-word milestone by name | +| `%"release candidate"` | multi-word milestone by name | +| `9ba12248` | specific commit | +| `9ba12248...b19a04f5` | commit range comparison | +| `[README](doc/README)` | repository file references | GFM also recognizes certain cross-project references: @@ -206,6 +223,7 @@ GFM also recognizes certain cross-project references: |:----------------------------------------|:------------------------| | `namespace/project#123` | issue | | `namespace/project!123` | merge request | +| `namespace/project%123` | milestone | | `namespace/project$123` | snippet | | `namespace/project@9ba12248` | specific commit | | `namespace/project@9ba12248...b19a04f5` | commit range comparison | @@ -402,7 +420,7 @@ There are two ways to create links, inline-style and reference-style. [I'm a reference-style link][Arbitrary case-insensitive reference text] -[I'm a relative reference to a repository file](LICENSE) +[I'm a relative reference to a repository file](LICENSE)[^1] [You can use numbers for reference-style link definitions][1] @@ -594,3 +612,4 @@ By including colons in the header row, you can align the text within that column [rouge]: http://rouge.jneen.net/ "Rouge website" [redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website" +[^1]: This link will be broken if you see this document from the Help page or docs.gitlab.com diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md index 5ec0a2069b5..8f9ef054949 100644 --- a/doc/migrate_ci_to_ce/README.md +++ b/doc/migrate_ci_to_ce/README.md @@ -355,7 +355,7 @@ sudo chown git:git /var/opt/gitlab/gitlab-ci/builds ``` #### Problems when importing CI database to GitLab -If you were migrating CI database from MySQL to PostgreSQL manually you can see errros during import about missing sequences: +If you were migrating CI database from MySQL to PostgreSQL manually you can see errors during import about missing sequences: ``` ALTER SEQUENCE ERROR: relation "ci_builds_id_seq" does not exist diff --git a/doc/monitoring/health_check.md b/doc/monitoring/health_check.md new file mode 100644 index 00000000000..0d17799372f --- /dev/null +++ b/doc/monitoring/health_check.md @@ -0,0 +1,66 @@ +# Health Check + +>**Note:** This feature was [introduced][ce-3888] in GitLab 8.8. + +GitLab provides a health check endpoint for uptime monitoring on the `health_check` web +endpoint. The health check reports on the overall system status based on the status of +the database connection, the state of the database migrations, and the ability to write +and access the cache. This endpoint can be provided to uptime monitoring services like +[Pingdom][pingdom], [Nagios][nagios-health], and [NewRelic][newrelic-health]. + +## Access Token + +An access token needs to be provided while accessing the health check endpoint. The current +accepted token can be found on the `admin/health_check` page of your GitLab instance. + +![access token](img/health_check_token.png) + +The access token can be passed as a URL parameter: + +``` +https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN +``` + +or as an HTTP header: + +```bash +curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json +``` + +## Using the Endpoint + +Once you have the access token, health information can be retrieved as plain text, JSON, +or XML using the `health_check` endpoint: + +- `https://gitlab.example.com/health_check?token=ACCESS_TOKEN` +- `https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN` +- `https://gitlab.example.com/health_check.xml?token=ACCESS_TOKEN` + +You can also ask for the status of specific services: + +- `https://gitlab.example.com/health_check/cache.json?token=ACCESS_TOKEN` +- `https://gitlab.example.com/health_check/database.json?token=ACCESS_TOKEN` +- `https://gitlab.example.com/health_check/migrations.json?token=ACCESS_TOKEN` + +For example, the JSON output of the following health check: + +```bash +curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json +``` + +would be like: + +``` +{"healthy":true,"message":"success"} +``` + +## Status + +On failure, the endpoint will return a `500` HTTP status code. On success, the endpoint +will return a valid successful HTTP status code, and a `success` message. Ideally your +uptime monitoring should look for the success message. + +[ce-3888]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3888 +[pingdom]: https://www.pingdom.com +[nagios-health]: https://nagios-plugins.org/doc/man/check_http.html +[newrelic-health]: https://docs.newrelic.com/docs/alerts/alert-policies/downtime-alerts/availability-monitoring diff --git a/doc/monitoring/img/health_check_token.png b/doc/monitoring/img/health_check_token.png Binary files differnew file mode 100644 index 00000000000..2daf8606b00 --- /dev/null +++ b/doc/monitoring/img/health_check_token.png diff --git a/doc/operations/moving_repositories.md b/doc/operations/moving_repositories.md index 39086b7a251..54adb99386a 100644 --- a/doc/operations/moving_repositories.md +++ b/doc/operations/moving_repositories.md @@ -134,7 +134,7 @@ sudo -u git sh -c ' cat /var/opt/gitlab/transfer-logs/* | sort | uniq -u |\ /usr/bin/env JOBS=10 \ /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \ - /var/opt/gitlab/transfer-logs/succes-$(date +%s).log \ + /var/opt/gitlab/transfer-logs/success-$(date +%s).log \ /var/opt/gitlab/git-data/repositories \ /mnt/gitlab/repositories ' @@ -145,7 +145,7 @@ sudo -u git -H sh -c ' cat /home/git/transfer-logs/* | sort | uniq -u |\ /usr/bin/env JOBS=10 \ bin/parallel-rsync-repos \ - /home/git/transfer-logs/succes-$(date +%s).log \ + /home/git/transfer-logs/success-$(date +%s).log \ /home/git/repositories \ /mnt/gitlab/repositories ` @@ -164,7 +164,7 @@ sudo gitlab-rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\ sudo -u git \ /usr/bin/env JOBS=10 \ /opt/gitlab/embedded/service/gitlab-rails/bin/parallel-rsync-repos \ - succes-$(date +%s).log \ + success-$(date +%s).log \ /var/opt/gitlab/git-data/repositories \ /mnt/gitlab/repositories @@ -174,7 +174,7 @@ sudo -u git -H bundle exec rake gitlab:list_repos SINCE='2015-10-1 12:00 UTC' |\ sudo -u git -H \ /usr/bin/env JOBS=10 \ bin/parallel-rsync-repos \ - succes-$(date +%s).log \ + success-$(date +%s).log \ /home/git/repositories \ /mnt/gitlab/repositories ``` diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 6219693b8a8..b76ce31cbad 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -27,6 +27,7 @@ documentation](../workflow/add-user/add-user.md). | Manage issue tracker | | ✓ | ✓ | ✓ | ✓ | | Manage labels | | ✓ | ✓ | ✓ | ✓ | | See a commit status | | ✓ | ✓ | ✓ | ✓ | +| See a container registry | | ✓ | ✓ | ✓ | ✓ | | Manage merge requests | | | ✓ | ✓ | ✓ | | Create new merge request | | | ✓ | ✓ | ✓ | | Create new branches | | | ✓ | ✓ | ✓ | @@ -37,6 +38,8 @@ documentation](../workflow/add-user/add-user.md). | Write a wiki | | | ✓ | ✓ | ✓ | | Cancel and retry builds | | | ✓ | ✓ | ✓ | | Create or update commit status | | | ✓ | ✓ | ✓ | +| Update a container registry | | | ✓ | ✓ | ✓ | +| Remove a container registry image | | | ✓ | ✓ | ✓ | | Create new milestones | | | | ✓ | ✓ | | Add new team members | | | | ✓ | ✓ | | Push to protected branches | | | | ✓ | ✓ | diff --git a/doc/profile/2fa_u2f_authenticate.png b/doc/profile/2fa_u2f_authenticate.png Binary files differnew file mode 100644 index 00000000000..b9138ff60db --- /dev/null +++ b/doc/profile/2fa_u2f_authenticate.png diff --git a/doc/profile/2fa_u2f_register.png b/doc/profile/2fa_u2f_register.png Binary files differnew file mode 100644 index 00000000000..15b3683ef73 --- /dev/null +++ b/doc/profile/2fa_u2f_register.png diff --git a/doc/profile/two_factor_authentication.md b/doc/profile/two_factor_authentication.md index a0e23c1586c..82505b13401 100644 --- a/doc/profile/two_factor_authentication.md +++ b/doc/profile/two_factor_authentication.md @@ -8,12 +8,27 @@ your phone. By enabling 2FA, the only way someone other than you can log into your account is to know your username and password *and* have access to your phone. -#### Note +> **Note:** When you enable 2FA, don't forget to back up your recovery codes. For your safety, if you lose your codes for GitLab.com, we can't disable or recover them. +In addition to a phone application, GitLab supports U2F (universal 2nd factor) devices as +the second factor of authentication. Once enabled, in addition to supplying your username and +password to login, you'll be prompted to activate your U2F device (usually by pressing +a button on it), and it will perform secure authentication on your behalf. + +> **Note:** Support for U2F devices was added in version 8.8 + +The U2F workflow is only supported by Google Chrome at this point, so we _strongly_ recommend +that you set up both methods of two-factor authentication, so you can still access your account +from other browsers. + +> **Note:** GitLab officially only supports [Yubikey] U2F devices. + ## Enabling 2FA +### Enable 2FA via mobile application + **In GitLab:** 1. Log in to your GitLab account. @@ -38,9 +53,26 @@ lose your codes for GitLab.com, we can't disable or recover them. 1. Click **Submit**. If the pin you entered was correct, you'll see a message indicating that -Two-factor Authentication has been enabled, and you'll be presented with a list +Two-Factor Authentication has been enabled, and you'll be presented with a list of recovery codes. +### Enable 2FA via U2F device + +**In GitLab:** + +1. Log in to your GitLab account. +1. Go to your **Profile Settings**. +1. Go to **Account**. +1. Click **Enable Two-Factor Authentication**. +1. Plug in your U2F device. +1. Click on **Setup New U2F Device**. +1. A light will start blinking on your device. Activate it by pressing its button. + +You will see a message indicating that your device was successfully set up. +Click on **Register U2F Device** to complete the process. + +![Two-Factor U2F Setup](2fa_u2f_register.png) + ## Recovery Codes Should you ever lose access to your phone, you can use one of the ten provided @@ -51,21 +83,39 @@ account. If you lose the recovery codes or just want to generate new ones, you can do so from the **Profile Settings** > **Account** page where you first enabled 2FA. +> **Note:** Recovery codes are not generated for U2F devices. + ## Logging in with 2FA Enabled Logging in with 2FA enabled is only slightly different than a normal login. Enter your username and password credentials as you normally would, and you'll -be presented with a second prompt for an authentication code. Enter the pin from -your phone's application or a recovery code to log in. +be presented with a second prompt, depending on which type of 2FA you've enabled. + +### Log in via mobile application + +Enter the pin from your phone's application or a recovery code to log in. -![Two-factor authentication on sign in](2fa_auth.png) +![Two-Factor Authentication on sign in via OTP](2fa_auth.png) + +### Log in via U2F device + +1. Click **Login via U2F Device** +1. A light will start blinking on your device. Activate it by pressing its button. + +You will see a message indicating that your device responded to the authentication request. +Click on **Authenticate via U2F Device** to complete the process. + +![Two-Factor Authentication on sign in via U2F device](2fa_u2f_authenticate.png) ## Disabling 2FA 1. Log in to your GitLab account. 1. Go to your **Profile Settings**. 1. Go to **Account**. -1. Click **Disable Two-factor Authentication**. +1. Click **Disable**, under **Two-Factor Authentication**. + +This will clear all your two-factor authentication registrations, including mobile +applications and U2F devices. ## Note to GitLab administrators @@ -74,3 +124,4 @@ You need to take special care to that 2FA keeps working after [Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en [FreeOTP]: https://fedorahosted.org/freeotp/ +[YubiKey]: https://www.yubico.com/products/yubikey-hardware/ diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md index 6be954ad68b..a49c43b8ef2 100644 --- a/doc/raketasks/README.md +++ b/doc/raketasks/README.md @@ -8,4 +8,4 @@ - [User management](user_management.md) - [Webhooks](web_hooks.md) - [Import](import.md) of git repositories in bulk -- [Rebuild authorized_keys file](http://doc.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators +- [Rebuild authorized_keys file](http://docs.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators diff --git a/doc/security/README.md b/doc/security/README.md index 4cd0fdd4094..38706e48ec5 100644 --- a/doc/security/README.md +++ b/doc/security/README.md @@ -8,3 +8,4 @@ - [User File Uploads](user_file_uploads.md) - [How we manage the CRIME vulnerability](crime_vulnerability.md) - [Enforce Two-factor authentication](two_factor_authentication.md) +- [Send email confirmation on sign-up](user_email_confirmation.md) diff --git a/doc/security/user_email_confirmation.md b/doc/security/user_email_confirmation.md new file mode 100644 index 00000000000..4293944ae8b --- /dev/null +++ b/doc/security/user_email_confirmation.md @@ -0,0 +1,7 @@ +# User email confirmation at sign-up + +Gitlab admin can enable email confirmation on sign-up, if you want to confirm all +user emails before they are able to sign-in. + +In the Admin area under **Settings** (`/admin/application_settings`), go to section +**Sign-in Restrictions** and look for **Send confirmation email on sign-up** option. diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md index 4a2c6ea91d2..bb463d43a7c 100644 --- a/doc/update/8.6-to-8.7.md +++ b/doc/update/8.6-to-8.7.md @@ -86,6 +86,14 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS ### 7. Update configuration files +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +git diff origin/8-6-stable:config/gitlab.yml.example origin/8-7-stable:config/gitlab.yml.example +``` + #### Git configuration Disable `git gc --auto` because GitLab runs `git gc` for us already. diff --git a/doc/update/8.7-to-8.8.md b/doc/update/8.7-to-8.8.md new file mode 100644 index 00000000000..32906650f6f --- /dev/null +++ b/doc/update/8.7-to-8.8.md @@ -0,0 +1,162 @@ +# From 8.7 to 8.8 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 8-8-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-8-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v2.7.2 +``` + +### 5. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch --all +sudo -u git -H git checkout v0.7.1 +sudo -u git -H make +``` + +### 6. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +``` + +### 7. Update configuration files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +git diff origin/8-7-stable:config/gitlab.yml.example origin/8-8-stable:config/gitlab.yml.example +``` + +#### Git configuration + +Disable `git gc --auto` because GitLab runs `git gc` for us already. + +```sh +sudo -u git -H git config --global gc.auto 0 +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +# For HTTPS configurations +git diff origin/8-7-stable:lib/support/nginx/gitlab-ssl origin/8-8-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-7-stable:lib/support/nginx/gitlab origin/8-8-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/lib/support/init.d/gitlab.default.example#L37 + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + + sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab + +### 8. Start application + + sudo service gitlab start + sudo service nginx restart + +### 9. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.7) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.6 to 8.7](8.6-to-8.7.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/update/8.8-to-8.9.md b/doc/update/8.8-to-8.9.md new file mode 100644 index 00000000000..f14046bb4be --- /dev/null +++ b/doc/update/8.8-to-8.9.md @@ -0,0 +1,162 @@ +# From 8.8 to 8.9 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 8-9-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-9-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v3.0.0 +``` + +### 5. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch --all +sudo -u git -H git checkout v0.7.5 +sudo -u git -H make +``` + +### 6. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +``` + +### 7. Update configuration files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +git diff origin/8-8-stable:config/gitlab.yml.example origin/8-9-stable:config/gitlab.yml.example +``` + +#### Git configuration + +Disable `git gc --auto` because GitLab runs `git gc` for us already. + +```sh +sudo -u git -H git config --global gc.auto 0 +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +# For HTTPS configurations +git diff origin/8-8-stable:lib/support/nginx/gitlab-ssl origin/8-9-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-8-stable:lib/support/nginx/gitlab origin/8-9-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-9-stable/lib/support/init.d/gitlab.default.example#L37 + +#### Init script + +Ensure you're still up-to-date with the latest init script changes: + + sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab + +### 8. Start application + + sudo service gitlab start + sudo service nginx restart + +### 9. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.8) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.7 to 8.8](8.7-to-8.8.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/update/README.md b/doc/update/README.md index a770633c9b8..975d72164b4 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -29,7 +29,7 @@ Based on your installation, choose a section below that fits your needs. ## Omnibus Packages -- The [Omnibus update guide](http://doc.gitlab.com/omnibus/update/README.html) +- The [Omnibus update guide](http://docs.gitlab.com/omnibus/update/README.html) contains the steps needed to update an Omnibus GitLab package. ## Installation from source @@ -86,10 +86,10 @@ possible. information about configuring GitLab to work with a MySQL database. - [Restoring from backup after a failed upgrade](restore_after_failure.md) -[omnidocker]: http://doc.gitlab.com/omnibus/docker/README.html +[omnidocker]: http://docs.gitlab.com/omnibus/docker/README.html [source-ee]: https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc/update [source-ce]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update [ee-ce]: ../downgrade_ee_to_ce/README.md [ce]: https://about.gitlab.com/features/#community [ee]: https://about.gitlab.com/features/#enterprise -[omni-ce-ee]: http://doc.gitlab.com/omnibus/update/README.html#from-community-edition-to-enterprise-edition +[omni-ce-ee]: http://docs.gitlab.com/omnibus/update/README.html#from-community-edition-to-enterprise-edition diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index b4283a526f3..60729316cde 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -57,10 +57,10 @@ sudo -u git -H make cd /home/git/gitlab # PostgreSQL -sudo -u git -H bundle install --without development test mysql --with postgres --deployment +sudo -u git -H bundle install --without development test mysql --deployment # MySQL -sudo -u git -H bundle install --without development test postgres --with mysql --deployment +sudo -u git -H bundle install --without development test postgres --deployment # Optional: clean up old gems sudo -u git -H bundle clean diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index c1c51302e79..8559b67af04 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -13,6 +13,19 @@ You can configure webhooks to listen for specific events like pushes, issues or Webhooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server. +## Webhook endpoint tips + +If you are writing your own endpoint (web server) that will receive +GitLab webhooks keep in mind the following things: + +- Your endpoint should send its HTTP response as fast as possible. If + you wait too long, GitLab may decide the hook failed and retry it. +- Your endpoint should ALWAYS return a valid HTTP response. If you do + not do this then GitLab will think the hook failed and retry it. + Most HTTP libraries take care of this for you automatically but if + you are writing a low-level hook this is important to remember. +- GitLab ignores the HTTP status code returned by your endpoint. + ## SSL Verification By default, the SSL certificate of the webhook endpoint is verified based on @@ -682,6 +695,61 @@ X-Gitlab-Event: Merge Request Hook } ``` +## Wiki Page events + +Triggered when a wiki page is created or edited. + +**Request Header**: + +``` +X-Gitlab-Event: Wiki Page Hook +``` + +**Request Body**: + +```json +{ + "object_kind": "wiki_page", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" + }, + "project": { + "name": "awesome-project", + "description": "This is awesome", + "web_url": "http://example.com/root/awesome-project", + "avatar_url": null, + "git_ssh_url": "git@example.com:root/awesome-project.git", + "git_http_url": "http://example.com/root/awesome-project.git", + "namespace": "root", + "visibility_level": 0, + "path_with_namespace": "root/awesome-project", + "default_branch": "master", + "homepage": "http://example.com/root/awesome-project", + "url": "git@example.com:root/awesome-project.git", + "ssh_url": "git@example.com:root/awesome-project.git", + "http_url": "http://example.com/root/awesome-project.git" + }, + "wiki": { + "web_url": "http://example.com/root/awesome-project/wikis/home", + "git_ssh_url": "git@example.com:root/awesome-project.wiki.git", + "git_http_url": "http://example.com/root/awesome-project.wiki.git", + "path_with_namespace": "root/awesome-project.wiki", + "default_branch": "master" + }, + "object_attributes": { + "title": "Awesome", + "content": "awesome content goes here", + "format": "markdown", + "message": "adding an awesome page to the wiki", + "slug": "awesome", + "url": "http://example.com/root/awesome-project/wikis/awesome", + "action": "create" + } +} +``` + #### Example webhook receiver If you want to see GitLab's webhooks in action for testing purposes you can use diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 1b354bcc0f1..2b2f140f8bf 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -131,7 +131,7 @@ When you feel comfortable with it to be merged you assign it to the person that There is room for more feedback and after the assigned person feels comfortable with the result the branch is merged. If the assigned person does not feel comfortable they can close the merge request without merging. -In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://doc.gitlab.com/ce/permissions/permissions.html). +In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://docs.gitlab.com/ce/permissions/permissions.html). So if you want to merge it into a protected branch you assign it to someone with master authorizations. ## Issues with GitLab flow @@ -187,7 +187,7 @@ If you have an issue that spans across multiple repositories, the best thing is ![Vim screen showing the rebase view](rebase.png) With git you can use an interactive rebase (`rebase -i`) to squash multiple commits into one and reorder them. -In GitLab EE and .com you can also [rebase before merge](http://doc.gitlab.com/ee/workflow/rebase_before_merge.html) from the web interface. +In GitLab EE and .com you can also [rebase before merge](http://docs.gitlab.com/ee/workflow/rebase_before_merge.html) from the web interface. This functionality is useful if you made a couple of commits for small changes during development and want to replace them with a single commit or if you want to make the order more logical. However you should never rebase commits you have pushed to a remote server. Somebody can have referred to the commits or cherry-picked them. diff --git a/doc/workflow/groups.md b/doc/workflow/groups.md index 52bf611dc5e..34ada1774d8 100644 --- a/doc/workflow/groups.md +++ b/doc/workflow/groups.md @@ -54,7 +54,7 @@ If necessary, you can increase the access level of an individual user for a spec ## Managing group memberships via LDAP In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups. -See [the GitLab Enterprise Edition documentation](http://doc.gitlab.com/ee/integration/ldap.html) for more information. +See [the GitLab Enterprise Edition documentation](http://docs.gitlab.com/ee/integration/ldap.html) for more information. ## Allowing only admins to create groups diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md index e670e415c71..a7dfac2c120 100644 --- a/doc/workflow/importing/import_projects_from_github.md +++ b/doc/workflow/importing/import_projects_from_github.md @@ -44,5 +44,5 @@ case the namespace is taken, the project will be imported on the user's namespace.
[gh-import]: ../../integration/github.md "GitHub integration"
-[ee-gh]: http://doc.gitlab.com/ee/integration/github.html "GitHub integration for GitLab EE"
+[ee-gh]: http://docs.gitlab.com/ee/integration/github.html "GitHub integration for GitLab EE"
[new-project]: ../../gitlab-basics/create-project.md "How to create a new project in GitLab"
diff --git a/doc/workflow/importing/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md index 1117db98e7e..dcc00074b75 100644 --- a/doc/workflow/importing/import_projects_from_gitlab_com.md +++ b/doc/workflow/importing/import_projects_from_gitlab_com.md @@ -2,7 +2,7 @@ You can import your existing GitLab.com projects to your GitLab instance. But keep in mind that it is possible only if GitLab support is enabled on your GitLab instance. -You can read more about GitLab support [here](http://doc.gitlab.com/ce/integration/gitlab.html) +You can read more about GitLab support [here](http://docs.gitlab.com/ce/integration/gitlab.html) To get to the importer page you need to go to "New project" page. ![New project page](gitlab_importer/new_project_page.png) diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index 31620044b15..9fe065fa680 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -4,7 +4,7 @@ Managing large files such as audio, video and graphics files has always been one of the shortcomings of Git. The general recommendation is to not have Git repositories larger than 1GB to preserve performance. -GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html) +GitLab already supports [managing large files with git annex](http://docs.gitlab.com/ee/workflow/git_annex.html) (EE only), however in certain environments it is not always convenient to use different commands to differentiate between the large files and regular ones. @@ -127,7 +127,7 @@ To prevent this from happening, set the lfs url in project Git config: ```bash -git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs/objects/batch" +git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs" ``` ### Credentials are always required when pushing an object @@ -152,4 +152,4 @@ If you are using OS X you can use `osxkeychain` to store and encrypt your creden For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases). More details about various methods of storing the user credentials can be found -on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
\ No newline at end of file +on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md index 1b5718c91c1..d2ec56e6504 100644 --- a/doc/workflow/merge_requests.md +++ b/doc/workflow/merge_requests.md @@ -2,6 +2,17 @@ Merge requests allow you to exchange changes you made to source code +## Only allow merge requests to be merged if the build succeeds + +You can prevent merge requests from being merged if their build did not succeed +in the project settings page. + +![only_allow_merge_if_build_succeeds](merge_requests/only_allow_merge_if_build_succeeds.png) + +Navigate to project settings page and select the `Only allow merge requests to be merged if the build succeeds` check box. + +Please note that you need to have builds configured to enable this feature. + ## Checkout merge requests locally Locate the section for your GitLab remote in the `.git/config` file. It looks like this: diff --git a/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png b/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png Binary files differnew file mode 100644 index 00000000000..18bebf5fe92 --- /dev/null +++ b/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index 80817c98d22..cbca94c0b5e 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -69,7 +69,7 @@ In all of the below cases, the notification will be sent to: ...with notification level "Participating" or higher -- Watchers: project members with notification level "Watch" +- Watchers: users with notification level "Watch" - Subscribers: anyone who manually subscribed to the issue/merge request | Event | Sent to | diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index 2fd097d100b..c4f987a7923 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -10,14 +10,9 @@ Feature: Project Active Tab Then the active main tab should be Home And no other main tabs should be active - Scenario: On Project Files + Scenario: On Project Code Given I visit my project's files page - Then the active main tab should be Files - And no other main tabs should be active - - Scenario: On Project Commits - Given I visit my project's commits page - Then the active main tab should be Commits + Then the active main tab should be Code And no other main tabs should be active Scenario: On Project Issues @@ -30,11 +25,6 @@ Feature: Project Active Tab Then the active main tab should be Merge Requests And no other main tabs should be active - Scenario: On Project Members - Given I visit my project's members page - Then the active main tab should be Members - And no other main tabs should be active - Scenario: On Project Wiki Given I visit my project's wiki page Then the active main tab should be Wiki @@ -49,13 +39,6 @@ Feature: Project Active Tab # Sub Tabs: Settings - Scenario: On Project Settings/Edit - Given I visit my project's settings page - And I click the "Edit" tab - Then the active sub nav should be Edit - And no other sub navs should be active - And the active main tab should be Settings - Scenario: On Project Settings/Hooks Given I visit my project's settings page And I click the "Hooks" tab @@ -70,40 +53,52 @@ Feature: Project Active Tab And no other sub navs should be active And the active main tab should be Settings - # Sub Tabs: Commits + Scenario: On Project Members + Given I visit my project's members page + Then the active sub nav should be Members + And no other sub navs should be active + And the active main tab should be Settings - Scenario: On Project Commits/Commits + # Sub Tabs: Code + + Scenario: On Project Code/Files + Given I visit my project's files page + Then the active sub tab should be Files + And no other sub tabs should be active + And the active main tab should be Code + + Scenario: On Project Code/Commits Given I visit my project's commits page Then the active sub tab should be Commits And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Network + Scenario: On Project Code/Network Given I visit my project's network page Then the active sub tab should be Network And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Compare + Scenario: On Project Code/Compare Given I visit my project's commits page And I click the "Compare" tab Then the active sub tab should be Compare And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Branches + Scenario: On Project Code/Branches Given I visit my project's commits page And I click the "Branches" tab Then the active sub tab should be Branches And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Tags + Scenario: On Project Code/Tags Given I visit my project's commits page And I click the "Tags" tab Then the active sub tab should be Tags And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code Scenario: On Project Issues/Browse Given I visit my project's issues page @@ -112,12 +107,16 @@ Feature: Project Active Tab Scenario: On Project Issues/Milestones Given I visit my project's issues page - And I click the "Milestones" tab - Then the active main tab should be Milestones + And I click the "Milestones" sub tab + Then the active main tab should be Issues + Then the active sub tab should be Milestones And no other main tabs should be active + And no other sub tabs should be active Scenario: On Project Issues/Labels Given I visit my project's issues page - And I click the "Labels" tab - Then the active main tab should be Labels + And I click the "Labels" sub tab + Then the active main tab should be Issues + Then the active sub tab should be Labels And no other main tabs should be active + And no other sub tabs should be active diff --git a/features/project/builds/summary.feature b/features/project/builds/summary.feature index 3c029a973df..550ebccf0d7 100644 --- a/features/project/builds/summary.feature +++ b/features/project/builds/summary.feature @@ -24,3 +24,4 @@ Feature: Project Builds Summary Then recent build has been erased And recent build summary does not have artifacts widget And recent build summary contains information saying that build has been erased + And the build count cache is updated diff --git a/features/project/commits/tags.feature b/features/project/commits/tags.feature deleted file mode 100644 index a4be39b2d40..00000000000 --- a/features/project/commits/tags.feature +++ /dev/null @@ -1,46 +0,0 @@ -@project_commits -Feature: Project Commits Tags - Background: - Given I sign in as a user - And I own project "Shop" - Given I visit project tags page - - Scenario: I can see all git tags - Then I should see "Shop" all tags list - - Scenario: I create a tag - And I click new tag link - And I submit new tag form - Then I should see new tag created - - Scenario: I create a tag with release notes - Given I click new tag link - And I submit new tag form with release notes - Then I should see new tag created - And I should see tag release notes - - Scenario: I create a tag with invalid name - And I click new tag link - And I submit new tag form with invalid name - Then I should see new an error that tag is invalid - - Scenario: I create a tag with invalid reference - And I click new tag link - And I submit new tag form with invalid reference - Then I should see new an error that tag ref is invalid - - Scenario: I create a tag that already exists - And I click new tag link - And I submit new tag form with tag that already exists - Then I should see new an error that tag already exists - - Scenario: I delete a tag - Given I visit tag 'v1.1.0' page - Given I delete tag 'v1.1.0' - Then I should not see tag 'v1.1.0' - - Scenario: I add release notes to the tag - Given I visit tag 'v1.1.0' page - When I click edit tag link - And I fill release notes and submit form - Then I should see tag release notes diff --git a/features/project/create.feature b/features/project/create.feature index 27136798e36..67336d73bf7 100644 --- a/features/project/create.feature +++ b/features/project/create.feature @@ -7,20 +7,8 @@ Feature: Project Create @javascript Scenario: User create a project Given I sign in as a user - When I visit new project page - And I have an ssh key - And fill project form with valid data - Then I should see project page - And I should see empty project instuctions - - @javascript - Scenario: Empty project instructions - Given I sign in as a user And I have an ssh key When I visit new project page And fill project form with valid data - Then I see empty project instuctions - And I click on HTTP - Then Remote url should update to http link - And If I click on SSH - Then Remote url should update to ssh link + Then I should see project page + And I should see empty project instructions diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index de7e2b37725..2259b7125c4 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -25,13 +25,6 @@ Feature: Project Issues Scenario: I visit issue page Given I click link "Release 0.4" Then I should see issue "Release 0.4" - And I should see "1 of 2" in the sidebar - - Scenario: I navigate between issues - Given I click link "Release 0.4" - Then I click link "Next" in the sidebar - Then I should see issue "Tweet control" - And I should see "2 of 2" in the sidebar @javascript Scenario: I filter by author diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index ecda4ea8240..0e97e4d5954 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -49,14 +49,12 @@ Feature: Project Merge Requests Scenario: I visit an open merge request page Given I click link "Bug NS-04" Then I should see merge request "Bug NS-04" - And I should see "1 of 1" in the sidebar Scenario: I visit a merged merge request page Given project "Shop" have "Feature NS-05" merged merge request And I click link "Merged" And I click link "Feature NS-05" Then I should see merge request "Feature NS-05" - And I should see "3 of 3" in the sidebar Scenario: I close merge request page Given I click link "Bug NS-04" @@ -76,18 +74,6 @@ Feature: Project Merge Requests And I submit new merge request "Wiki Feature" Then I should see merge request "Wiki Feature" - Scenario: I download a diff on a public merge request - Given public project "Community" - And "John Doe" owns public project "Community" - And project "Community" has "Bug CO-01" open merge request with diffs inside - Given I logout directly - And I visit merge request page "Bug CO-01" - And I click on "Email Patches" - Then I should see a patch diff - And I visit merge request page "Bug CO-01" - And I click on "Plain Diff" - Then I should see a patch diff - @javascript Scenario: I comment on a merge request Given I visit merge request page "Bug NS-04" diff --git a/features/project/project.feature b/features/project/project.feature index f1f3ed26065..aa22401c88e 100644 --- a/features/project/project.feature +++ b/features/project/project.feature @@ -18,15 +18,6 @@ Feature: Project Then I should see the default project avatar And I should not see the "Remove avatar" button - Scenario: I should have back to group button - And project "Shop" belongs to group - And I visit project "Shop" page - Then I should see back to group button - - Scenario: I should have back to group button - And I visit project "Shop" page - Then I should see back to dashboard button - Scenario: I should have readme on page And I visit project "Shop" page Then I should see project "Shop" README diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature index 10e7c234610..c73d0b32337 100644 --- a/features/project/shortcuts.feature +++ b/features/project/shortcuts.feature @@ -8,19 +8,21 @@ Feature: Project Shortcuts @javascript Scenario: Navigate to files tab Given I press "g" and "f" - Then the active main tab should be Files + Then the active main tab should be Code + Then the active sub tab should be Files @javascript Scenario: Navigate to commits tab Given I visit my project's files page Given I press "g" and "c" - Then the active main tab should be Commits + Then the active main tab should be Code + Then the active sub tab should be Commits @javascript Scenario: Navigate to network tab Given I press "g" and "n" Then the active sub tab should be Network - And the active main tab should be Commits + And the active main tab should be Code @javascript Scenario: Navigate to graphs tab diff --git a/features/steps/admin/active_tab.rb b/features/steps/admin/active_tab.rb index 90d13abdb13..f2db1801389 100644 --- a/features/steps/admin/active_tab.rb +++ b/features/steps/admin/active_tab.rb @@ -1,7 +1,7 @@ class Spinach::Features::AdminActiveTab < Spinach::FeatureSteps include SharedAuthentication include SharedPaths - include SharedActiveTab + include SharedSidebarActiveTab step 'the active main tab should be Home' do ensure_active_main_tab('Overview') @@ -34,4 +34,12 @@ class Spinach::Features::AdminActiveTab < Spinach::FeatureSteps step 'the active main tab should be Messages' do ensure_active_main_tab('Messages') end + + step 'no other main tabs should be active' do + expect(page).to have_selector('.nav-sidebar > li.active', count: 1) + end + + def ensure_active_main_tab(content) + expect(find('.nav-sidebar > li.active')).to have_content(content) + end end diff --git a/features/steps/admin/users.rb b/features/steps/admin/users.rb index 4bc290b6bdf..8fb8a86d58b 100644 --- a/features/steps/admin/users.rb +++ b/features/steps/admin/users.rb @@ -158,7 +158,7 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps step 'I should not see twitter details' do expect(page).to have_content 'Pete' - expect(page).to_not have_content 'twitter' + expect(page).not_to have_content 'twitter' end step 'click on ssh keys tab' do diff --git a/features/steps/dashboard/active_tab.rb b/features/steps/dashboard/active_tab.rb index 0e2c04fb299..04fe96cef22 100644 --- a/features/steps/dashboard/active_tab.rb +++ b/features/steps/dashboard/active_tab.rb @@ -1,9 +1,5 @@ class Spinach::Features::DashboardActiveTab < Spinach::FeatureSteps include SharedAuthentication include SharedPaths - include SharedActiveTab - - step 'the active main tab should be Help' do - ensure_active_main_tab('Help') - end + include SharedSidebarActiveTab end diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index b5980b35102..80ed4c6d64c 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -13,7 +13,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps end step 'I should see "Shop" project CI status' do - expect(page).to have_link "Build skipped" + expect(page).to have_link "Commit: skipped" end step 'I should see last push widget' do diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb index e21af72a777..8706f0e8e78 100644 --- a/features/steps/dashboard/issues.rb +++ b/features/steps/dashboard/issues.rb @@ -74,7 +74,7 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps def project @project ||= begin - project =create :project + project = create :project project.team << [current_user, :master] project end diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb index a2adc87f8ef..06db36c7014 100644 --- a/features/steps/dashboard/merge_requests.rb +++ b/features/steps/dashboard/merge_requests.rb @@ -100,7 +100,7 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps def project @project ||= begin - project =create :project + project = create :project project.team << [current_user, :master] project end diff --git a/features/steps/dashboard/shortcuts.rb b/features/steps/dashboard/shortcuts.rb index a9083850b52..118d27888df 100644 --- a/features/steps/dashboard/shortcuts.rb +++ b/features/steps/dashboard/shortcuts.rb @@ -2,5 +2,6 @@ class Spinach::Features::DashboardShortcuts < Spinach::FeatureSteps include SharedAuthentication include SharedPaths include SharedProject - include SharedActiveTab + include SharedSidebarActiveTab + include SharedShortcuts end diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb index 2b23df6764b..19fedfbfcdf 100644 --- a/features/steps/dashboard/todos.rb +++ b/features/steps/dashboard/todos.rb @@ -20,13 +20,12 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps step 'I have todos' do create(:todo, user: current_user, project: project, author: mary_jane, target: issue, action: Todo::MENTIONED) create(:todo, user: current_user, project: project, author: john_doe, target: issue, action: Todo::ASSIGNED) - note = create(:note, author: john_doe, noteable: issue, note: "#{current_user.to_reference} Wdyt?") + note = create(:note, author: john_doe, noteable: issue, note: "#{current_user.to_reference} Wdyt?", project: project) create(:todo, user: current_user, project: project, author: john_doe, target: issue, action: Todo::MENTIONED, note: note) create(:todo, user: current_user, project: project, author: john_doe, target: merge_request, action: Todo::ASSIGNED) end step 'I should see todos assigned to me' do - page.within('.nav-sidebar') { expect(page).to have_content 'Todos 4' } expect(page).to have_content 'To do 4' expect(page).to have_content 'Done 0' @@ -42,7 +41,6 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps click_link 'Done' end - page.within('.nav-sidebar') { expect(page).to have_content 'Todos 3' } expect(page).to have_content 'To do 3' expect(page).to have_content 'Done 1' should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}" @@ -106,7 +104,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps if pending expect(page).to have_link 'Done' else - expect(page).to_not have_link 'Done' + expect(page).not_to have_link 'Done' end end end diff --git a/features/steps/profile/active_tab.rb b/features/steps/profile/active_tab.rb index 3b59089a093..4724a326277 100644 --- a/features/steps/profile/active_tab.rb +++ b/features/steps/profile/active_tab.rb @@ -22,8 +22,4 @@ class Spinach::Features::ProfileActiveTab < Spinach::FeatureSteps step 'the active main tab should be Audit Log' do ensure_active_main_tab('Audit Log') end - - def ensure_active_main_tab(content) - expect(find('.layout-nav li.active')).to have_content(content) - end end diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 909de31a479..9e5602dacf1 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -155,6 +155,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I click on my profile picture' do + find(:css, '.side-nav-toggle').click find(:css, '.sidebar-user').click end @@ -166,7 +167,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I have group with projects' do - @group = create(:group) + @group = create(:group) @group.add_owner(current_user) @project = create(:project, namespace: @group) @event = create(:closed_issue_event, project: @project) diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 19d81453d8c..80043463188 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -16,12 +16,14 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps end step 'I click the "Snippets" tab' do - click_link('Snippets') + page.within('.layout-nav') do + click_link('Snippets') + end end - step 'I click the "Edit" tab' do - page.within '.sidebar-subnav' do - click_link('Project Settings') + step 'I click the "Edit Project"' do + page.within '.layout-nav .controls' do + click_link('Edit Project') end end @@ -33,14 +35,10 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps click_link('Deploy Keys') end - step 'the active sub nav should be Team' do + step 'the active sub nav should be Members' do ensure_active_sub_nav('Members') end - step 'the active sub nav should be Edit' do - ensure_active_sub_nav('Project') - end - step 'the active sub nav should be Hooks' do ensure_active_sub_nav('Webhooks') end @@ -56,17 +54,15 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps end step 'I click the "Branches" tab' do - click_link('Branches') + page.within '.content' do + click_link('Branches') + end end step 'I click the "Tags" tab' do click_link('Tags') end - step 'the active sub tab should be Commits' do - ensure_active_sub_tab('Commits') - end - step 'the active sub tab should be Compare' do ensure_active_sub_tab('Compare') end @@ -81,23 +77,27 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps # Sub Tabs: Issues - step 'I click the "Milestones" tab' do - click_link('Milestones') + step 'I click the "Milestones" sub tab' do + page.within('.sub-nav') do + click_link('Milestones') + end end - step 'I click the "Labels" tab' do - click_link('Labels') + step 'I click the "Labels" sub tab' do + page.within('.sub-nav') do + click_link('Labels') + end end step 'the active sub tab should be Issues' do ensure_active_sub_tab('Issues') end - step 'the active main tab should be Milestones' do - ensure_active_main_tab('Milestones') + step 'the active sub tab should be Milestones' do + ensure_active_sub_tab('Milestones') end - step 'the active main tab should be Labels' do - ensure_active_main_tab('Labels') + step 'the active sub tab should be Labels' do + ensure_active_sub_tab('Labels') end end diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb index 1bdb57af9d1..2876e8812e9 100644 --- a/features/steps/project/builds/artifacts.rb +++ b/features/steps/project/builds/artifacts.rb @@ -5,11 +5,11 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps include RepoHelpers step 'I click artifacts download button' do - page.within('.artifacts') { click_link 'Download' } + click_link 'Download' end step 'I click artifacts browse button' do - page.within('.artifacts') { click_link 'Browse' } + click_link 'Browse' end step 'I should see content of artifacts archive' do diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb index e9e2359146e..374eb0b0e07 100644 --- a/features/steps/project/builds/summary.rb +++ b/features/steps/project/builds/summary.rb @@ -36,4 +36,8 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps expect(page).to have_content 'Build has been erased' end end + + step 'the build count cache is updated' do + expect(@build.project.running_or_pending_build_count).to eq @build.project.builds.running_or_pending.count(:all) + end end diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index 93c37bf507f..239036e431d 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -105,7 +105,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I should not see button to create a new merge request' do - expect(page).to_not have_link 'Create Merge Request' + expect(page).not_to have_link 'Create Merge Request' end step 'I should see button to the merge request' do @@ -164,16 +164,16 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps step 'commit has ci status' do @project.enable_ci - ci_commit = create :ci_commit, project: @project, sha: sample_commit.id - create :ci_build, commit: ci_commit + pipeline = create :ci_pipeline, project: @project, sha: sample_commit.id + create :ci_build, pipeline: pipeline end step 'repository contains ".gitlab-ci.yml" file' do - allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file).and_return(String.new) + allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file).and_return(String.new) end step 'I see commit ci info' do - expect(page).to have_content "build: pending" + expect(page).to have_content "Builds for 1 pipeline pending" end step 'I click status link' do @@ -181,7 +181,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I see builds list' do - expect(page).to have_content "build: pending" + expect(page).to have_content "Builds for 1 pipeline pending" expect(page).to have_content "1 build" end diff --git a/features/steps/project/commits/tags.rb b/features/steps/project/commits/tags.rb deleted file mode 100644 index 912ba580efd..00000000000 --- a/features/steps/project/commits/tags.rb +++ /dev/null @@ -1,90 +0,0 @@ -class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - - step 'I should see "Shop" all tags list' do - expect(page).to have_content "Tags" - expect(page).to have_content "v1.0.0" - end - - step 'I click new tag link' do - click_link 'New tag' - end - - step 'I submit new tag form' do - fill_in 'tag_name', with: 'v7.0' - fill_in 'ref', with: 'master' - click_button 'Create tag' - end - - step 'I submit new tag form with release notes' do - fill_in 'tag_name', with: 'v7.0' - fill_in 'ref', with: 'master' - fill_in 'release_description', with: 'Awesome release notes' - click_button 'Create tag' - end - - step 'I fill release notes and submit form' do - fill_in 'release_description', with: 'Awesome release notes' - click_button 'Save changes' - end - - step 'I submit new tag form with invalid name' do - fill_in 'tag_name', with: 'v 1.0' - fill_in 'ref', with: 'master' - click_button 'Create tag' - end - - step 'I submit new tag form with invalid reference' do - fill_in 'tag_name', with: 'foo' - fill_in 'ref', with: 'foo' - click_button 'Create tag' - end - - step 'I submit new tag form with tag that already exists' do - fill_in 'tag_name', with: 'v1.0.0' - fill_in 'ref', with: 'master' - click_button 'Create tag' - end - - step 'I should see new tag created' do - expect(page).to have_content 'v7.0' - end - - step 'I should see new an error that tag is invalid' do - expect(page).to have_content 'Tag name invalid' - end - - step 'I should see new an error that tag ref is invalid' do - expect(page).to have_content 'Target foo is invalid' - end - - step 'I should see new an error that tag already exists' do - expect(page).to have_content 'Tag v1.0.0 already exists' - end - - step "I visit tag 'v1.1.0' page" do - click_link 'v1.1.0' - end - - step "I delete tag 'v1.1.0'" do - page.within('.content') do - first('.btn-remove').click - end - end - - step "I should not see tag 'v1.1.0'" do - page.within '.tags' do - expect(page).not_to have_link 'v1.1.0' - end - end - - step 'I click edit tag link' do - click_link 'Edit release notes' - end - - step 'I should see tag release notes' do - expect(page).to have_content 'Awesome release notes' - end -end diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb index 422b151eaa2..5f5f806df36 100644 --- a/features/steps/project/create.rb +++ b/features/steps/project/create.rb @@ -13,33 +13,9 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps expect(current_path).to eq namespace_project_path(Project.last.namespace, Project.last) end - step 'I should see empty project instuctions' do + step 'I should see empty project instructions' do expect(page).to have_content "git init" expect(page).to have_content "git remote" expect(page).to have_content Project.last.url_to_repo end - - step 'I see empty project instuctions' do - expect(page).to have_content "git init" - expect(page).to have_content "git remote" - expect(page).to have_content Project.last.url_to_repo - end - - step 'I click on HTTP' do - find('#clone-dropdown').click - find('.http-selector').click - end - - step 'Remote url should update to http link' do - expect(page).to have_content "git remote add origin #{Project.last.http_url_to_repo}" - end - - step 'If I click on SSH' do - find('#clone-dropdown').click - find('.ssh-selector').click - end - - step 'Remote url should update to ssh link' do - expect(page).to have_content "git remote add origin #{Project.last.url_to_repo}" - end end diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 527f7853da9..8abeb5ee242 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -36,7 +36,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I goto the Merge Requests page' do - page.within '.page-sidebar-expanded' do + page.within '.layout-nav' do click_link "Merge Requests" end end diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb index b1ffe7f7b4c..13c0713669a 100644 --- a/features/steps/project/hooks.rb +++ b/features/steps/project/hooks.rb @@ -59,7 +59,7 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps step 'hook should be triggered' do expect(current_path).to eq namespace_project_hooks_path(current_project.namespace, current_project) expect(page).to have_selector '.flash-notice', - text: 'Hook successfully executed.' + text: 'Hook executed successfully: HTTP 200' end step 'I should see hook error message' do diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb index c5d45709b44..1b14659b4df 100644 --- a/features/steps/project/issues/award_emoji.rb +++ b/features/steps/project/issues/award_emoji.rb @@ -39,8 +39,8 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps step 'I can see the activity and food categories' do page.within '.emoji-menu' do - expect(page).to_not have_selector 'Activity' - expect(page).to_not have_selector 'Food' + expect(page).not_to have_selector 'Activity' + expect(page).not_to have_selector 'Food' end end diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb index d82c6856918..d34fa694789 100644 --- a/features/steps/project/issues/filter_labels.rb +++ b/features/steps/project/issues/filter_labels.rb @@ -29,7 +29,7 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps end step 'I click link "bug"' do - page.find('.js-label-select').click + page.find('.js-label-select', visible: true).click sleep 0.5 execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()") end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index fc12843ea5c..439363e6f14 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -191,15 +191,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'issue "Release 0.4" have 2 upvotes and 1 downvote' do - issue = Issue.find_by(title: 'Release 0.4') - create_list(:upvote_note, 2, project: project, noteable: issue) - create(:downvote_note, project: project, noteable: issue) + awardable = Issue.find_by(title: 'Release 0.4') + create_list(:award_emoji, 2, awardable: awardable) + create(:award_emoji, :downvote, awardable: awardable) end step 'issue "Tweet control" have 1 upvote and 2 downvotes' do - issue = Issue.find_by(title: 'Tweet control') - create(:upvote_note, project: project, noteable: issue) - create_list(:downvote_note, 2, project: project, noteable: issue) + awardable = Issue.find_by(title: 'Tweet control') + create(:award_emoji, :upvote, awardable: awardable) + create_list(:award_emoji, 2, awardable: awardable, name: 'thumbsdown') end step 'The list should be sorted by "Least popular"' do @@ -216,7 +216,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps page.within 'li.issue:nth-child(3)' do expect(page).to have_content 'Bugfix' - expect(page).to_not have_content '0 0' + expect(page).not_to have_content '0 0' end end end @@ -235,7 +235,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps page.within 'li.issue:nth-child(3)' do expect(page).to have_content 'Bugfix' - expect(page).to_not have_content '0 0' + expect(page).not_to have_content '0 0' end end end @@ -348,7 +348,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps step 'another user adds a comment with text "Yay!" to issue "Release 0.4"' do issue = Issue.find_by!(title: 'Release 0.4') - create(:note_on_issue, noteable: issue, note: 'Yay!') + create(:note_on_issue, noteable: issue, project: project, note: 'Yay!') end step 'I should see a new comment with text "Yay!"' do diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb index 0ca2d6257c3..2937d5d7ca8 100644 --- a/features/steps/project/issues/labels.rb +++ b/features/steps/project/issues/labels.rb @@ -9,7 +9,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps step 'I remove label \'bug\'' do page.within "#label_#{bug_label.id}" do - click_link 'Delete' + first(:link, 'Delete').click end end @@ -24,8 +24,8 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps step 'I should see labels help message' do page.within '.labels' do - expect(page).to have_content 'Create first label or generate default set of '\ - 'labels' + expect(page).to have_content 'Create a label or generate a default set '\ + 'of labels' end end @@ -60,25 +60,25 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps end step 'I should see label \'feature\'' do - page.within '.manage-labels-list' do + page.within '.other-labels .manage-labels-list' do expect(page).to have_content 'feature' end end step 'I should see label \'bug\'' do - page.within '.manage-labels-list' do + page.within '.other-labels .manage-labels-list' do expect(page).to have_content 'bug' end end step 'I should not see label \'bug\'' do - page.within '.manage-labels-list' do + page.within '.other-labels .manage-labels-list' do expect(page).not_to have_content 'bug' end end step 'I should see label \'support\'' do - page.within '.manage-labels-list' do + page.within '.other-labels .manage-labels-list' do expect(page).to have_content 'support' end end @@ -90,7 +90,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps end step 'I should see label \'fix\'' do - page.within '.manage-labels-list' do + page.within '.other-labels .manage-labels-list' do expect(page).to have_content 'fix' end end diff --git a/features/steps/project/labels.rb b/features/steps/project/labels.rb index 5bb02189021..118ffef4774 100644 --- a/features/steps/project/labels.rb +++ b/features/steps/project/labels.rb @@ -29,6 +29,6 @@ class Spinach::Features::Labels < Spinach::FeatureSteps private def subscribe_button - first('.label-subscribe-button span') + first('.js-subscribe-button', visible: true) end end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 3b1a00f628a..640f1720a6c 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -179,14 +179,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'merge request "Bug NS-04" have 2 upvotes and 1 downvote' do merge_request = MergeRequest.find_by(title: 'Bug NS-04') - create_list(:upvote_note, 2, project: project, noteable: merge_request) - create(:downvote_note, project: project, noteable: merge_request) + create_list(:award_emoji, 2, awardable: merge_request) + create(:award_emoji, :downvote, awardable: merge_request) end step 'merge request "Bug NS-06" have 1 upvote and 2 downvotes' do - merge_request = MergeRequest.find_by(title: 'Bug NS-06') - create(:upvote_note, project: project, noteable: merge_request) - create_list(:downvote_note, 2, project: project, noteable: merge_request) + awardable = MergeRequest.find_by(title: 'Bug NS-06') + create(:award_emoji, awardable: awardable) + create_list(:award_emoji, 2, :downvote, awardable: awardable) end step 'The list should be sorted by "Least popular"' do @@ -203,7 +203,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps page.within 'li.merge-request:nth-child(3)' do expect(page).to have_content 'Bug NS-05' - expect(page).to_not have_content '0 0' + expect(page).not_to have_content '0 0' end end end @@ -222,7 +222,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps page.within 'li.merge-request:nth-child(3)' do expect(page).to have_content 'Bug NS-05' - expect(page).to_not have_content '0 0' + expect(page).not_to have_content '0 0' end end end @@ -273,7 +273,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'user "John Doe" leaves a comment like "Line is wrong" on diff' do mr = MergeRequest.find_by(title: "Bug NS-05") create(:note_on_merge_request_diff, project: project, - noteable_id: mr.id, + noteable: mr, author: user_exists("John Doe"), line_code: sample_commit.line_code, note: 'Line is wrong') @@ -519,13 +519,13 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step '"Bug NS-05" has CI status' do project = merge_request.source_project project.enable_ci - ci_commit = create :ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch - create :ci_build, commit: ci_commit + pipeline = create :ci_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch + create :ci_build, pipeline: pipeline end step 'I should see merge request "Bug NS-05" with CI status' do page.within ".mr-list" do - expect(page).to have_link "Build pending" + expect(page).to have_link "Pipeline: pending" end end @@ -567,7 +567,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps click_diff_line(sample_compare.changes[1][:line_code]) end - def have_visible_content (text) + def have_visible_content(text) have_css("*", text: text, visible: true) end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index ef185861e00..2a1a8e776f0 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -114,7 +114,9 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should not see "Snippets" button' do - expect(page).not_to have_link 'Snippets' + page.within '.content' do + expect(page).not_to have_link 'Snippets' + end end step 'project "Shop" belongs to group' do @@ -123,16 +125,8 @@ class Spinach::Features::Project < Spinach::FeatureSteps @project.save! end - step 'I should see back to dashboard button' do - expect(page).to have_content 'Go to dashboard' - end - - step 'I should see back to group button' do - expect(page).to have_content 'Go to group' - end - step 'I click notifications drop down button' do - click_link 'notifications-button' + find('#notifications-button').click end step 'I choose Mention setting' do diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb index 8c1d09d6cc6..47de4b91df1 100644 --- a/features/steps/project/project_find_file.rb +++ b/features/steps/project/project_find_file.rb @@ -13,12 +13,12 @@ class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps end step 'I should see "find file" page' do - ensure_active_main_tab('Files') + ensure_active_main_tab('Code') expect(page).to have_selector('.file-finder-holder', count: 1) end step 'I fill in Find by path with "git"' do - ensure_active_main_tab('Files') + ensure_active_main_tab('Code') expect(page).to have_selector('.file-finder-holder', count: 1) end diff --git a/features/steps/project/project_milestone.rb b/features/steps/project/project_milestone.rb index 2508c09e36d..1864b3a2b52 100644 --- a/features/steps/project/project_milestone.rb +++ b/features/steps/project/project_milestone.rb @@ -52,7 +52,7 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps end step 'I click link "Labels"' do - page.within('.nav-links') do + page.within('.layout-nav .nav-links') do page.find(:xpath, "//a[@href='#tab-labels']").click end end diff --git a/features/steps/project/project_shortcuts.rb b/features/steps/project/project_shortcuts.rb index 49e9c5520bb..8143b01ca40 100644 --- a/features/steps/project/project_shortcuts.rb +++ b/features/steps/project/project_shortcuts.rb @@ -3,6 +3,7 @@ class Spinach::Features::ProjectShortcuts < Spinach::FeatureSteps include SharedPaths include SharedProject include SharedProjectTab + include SharedShortcuts step 'I press "g" and "f"' do find('body').native.send_key('g') diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index 786a0cad975..beb8ecfc799 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -43,12 +43,12 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps step 'I click link "Edit"' do page.within ".detail-page-header" do - click_link "Edit" + first(:link, "Edit").click end end step 'I click link "Delete"' do - click_link "Delete" + first(:link, "Delete").click end step 'I submit new snippet "Snippet three"' do diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index c26d7a15212..2c0498de3b9 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -337,13 +337,15 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I should see buttons for allowed commands' do - expect(page).to have_content 'Raw' - expect(page).to have_content 'History' - expect(page).to have_content 'Permalink' - expect(page).not_to have_content 'Edit' - expect(page).not_to have_content 'Blame' - expect(page).to have_content 'Delete' - expect(page).to have_content 'Replace' + page.within '.content' do + expect(page).to have_content 'Raw' + expect(page).to have_content 'History' + expect(page).to have_content 'Permalink' + expect(page).not_to have_content 'Edit' + expect(page).not_to have_content 'Blame' + expect(page).to have_content 'Delete' + expect(page).to have_content 'Replace' + end end step 'I should see a notice about a new fork having been created' do diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index 3fbcf770b62..c6ced747370 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -126,7 +126,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps step 'I share project with group "OpenSource"' do project = Project.find_by(name: 'Shop') - os_group = create(:group, name: 'OpenSource') + os_group = create(:group, name: 'OpenSource') create(:project, group: os_group) @os_user1 = create(:user) @os_user2 = create(:user) diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index 9f6aed1c5b9..3cbf832c728 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -97,7 +97,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps file = Gollum::File.new(wiki.wiki) Gollum::Wiki.any_instance.stub(:file).with("image.jpg", "master", true).and_return(file) Gollum::File.any_instance.stub(:mime_type).and_return("image/jpeg") - expect(page).to have_link('image', href: "image.jpg") + expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/image.jpg") click_on "image" end @@ -113,7 +113,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I click on image link' do - expect(page).to have_link('image', href: "image.jpg") + expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/image.jpg") click_on "image" end diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb index 0bee91d758d..4eef7aff213 100644 --- a/features/steps/shared/active_tab.rb +++ b/features/steps/shared/active_tab.rb @@ -2,46 +2,26 @@ module SharedActiveTab include Spinach::DSL def ensure_active_main_tab(content) - expect(find('.nav-sidebar > li.active')).to have_content(content) + expect(find('.layout-nav li.active')).to have_content(content) end def ensure_active_sub_tab(content) - expect(find('div.content ul.nav-links li.active')).to have_content(content) + expect(find('.sub-nav li.active')).to have_content(content) end def ensure_active_sub_nav(content) - expect(find('.sidebar-subnav > li.active')).to have_content(content) + expect(find('.layout-nav .controls li.active')).to have_content(content) end step 'no other main tabs should be active' do - expect(page).to have_selector('.nav-sidebar > li.active', count: 1) + expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 1) end step 'no other sub tabs should be active' do - expect(page).to have_selector('div.content ul.nav-links li.active', count: 1) + expect(page).to have_selector('.sub-nav li.active', count: 1) end step 'no other sub navs should be active' do - expect(page).to have_selector('.sidebar-subnav > li.active', count: 1) - end - - step 'the active main tab should be Home' do - ensure_active_main_tab('Projects') - end - - step 'the active main tab should be Projects' do - ensure_active_main_tab('Projects') - end - - step 'the active main tab should be Issues' do - ensure_active_main_tab('Issues') - end - - step 'the active main tab should be Merge Requests' do - ensure_active_main_tab('Merge Requests') - end - - step 'the active main tab should be Help' do - ensure_active_main_tab('Help') + expect(page).to have_selector('.layout-nav .controls li.active', count: 1) end end diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb index cf30e23b6bd..4d6b258f577 100644 --- a/features/steps/shared/builds.rb +++ b/features/steps/shared/builds.rb @@ -10,8 +10,8 @@ module SharedBuilds end step 'project has a recent build' do - @ci_commit = create(:ci_commit, project: @project, sha: @project.commit.sha, ref: 'master') - @build = create(:ci_build_with_coverage, commit: @ci_commit) + @pipeline = create(:ci_pipeline, project: @project, sha: @project.commit.sha, ref: 'master') + @build = create(:ci_build_with_coverage, pipeline: @pipeline) end step 'recent build is successful' do @@ -23,7 +23,7 @@ module SharedBuilds end step 'project has another build that is running' do - create(:ci_build, commit: @ci_commit, name: 'second build', status: 'running') + create(:ci_build, pipeline: @pipeline, name: 'second build', status: 'running') end step 'I visit recent build details page' do diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index e846c52d474..e8b1e4b4879 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -23,7 +23,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}-true']") do fill_in "note[note]", with: "Typo, please fix" find(".js-comment-button").trigger("click") sleep 0.05 @@ -33,7 +33,7 @@ module SharedDiffNote step 'I leave a diff comment in a parallel view on the left side like "Old comment"' do click_parallel_diff_line(sample_commit.line_code, 'old') - page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}']") do + page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}-true']") do fill_in "note[note]", with: "Old comment" find(".js-comment-button").trigger("click") end @@ -41,7 +41,7 @@ module SharedDiffNote step 'I leave a diff comment in a parallel view on the right side like "New comment"' do click_parallel_diff_line(sample_commit.line_code, 'new') - page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}']") do + page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}-true']") do fill_in "note[note]", with: "New comment" find(".js-comment-button").trigger("click") end @@ -51,7 +51,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}-true']") do fill_in "note[note]", with: "Should fix it :smile:" find('.js-md-preview-button').click end @@ -62,7 +62,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.del_line_code) - page.within("form[id$='#{sample_commit.del_line_code}']") do + page.within("form[id$='#{sample_commit.del_line_code}-true']") do fill_in "note[note]", with: "DRY this up" find('.js-md-preview-button').click end @@ -91,7 +91,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}']") do + page.within("form[id$='#{sample_commit.line_code}-true']") do fill_in 'note[note]', with: ':smile:' click_button('Comment') end diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index a58b3cb7e16..c6572cf386e 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -111,7 +111,7 @@ module SharedIssuable step 'I sort the list by "Oldest updated"' do find('button.dropdown-toggle.btn').click - page.within('ul.dropdown-menu.dropdown-menu-align-right li') do + page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do click_link "Oldest updated" end end @@ -119,7 +119,7 @@ module SharedIssuable step 'I sort the list by "Least popular"' do find('button.dropdown-toggle.btn').click - page.within('ul.dropdown-menu.dropdown-menu-align-right li') do + page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do click_link 'Least popular' end end @@ -127,33 +127,17 @@ module SharedIssuable step 'I sort the list by "Most popular"' do find('button.dropdown-toggle.btn').click - page.within('ul.dropdown-menu.dropdown-menu-align-right li') do + page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do click_link 'Most popular' end end step 'The list should be sorted by "Oldest updated"' do - page.within('div.dropdown.inline.prepend-left-10') do + page.within('.content div.dropdown.inline.prepend-left-10') do expect(page.find('button.dropdown-toggle.btn')).to have_content('Oldest updated') end end - step 'I should see "1 of 1" in the sidebar' do - expect_sidebar_content('1 of 1') - end - - step 'I should see "1 of 2" in the sidebar' do - expect_sidebar_content('1 of 2') - end - - step 'I should see "2 of 2" in the sidebar' do - expect_sidebar_content('2 of 2') - end - - step 'I should see "3 of 3" in the sidebar' do - expect_sidebar_content('3 of 3') - end - step 'I click link "Next" in the sidebar' do page.within '.issuable-sidebar' do click_link 'Next' diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index a3c3887ab46..3d7c6ef9d2d 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -107,7 +107,7 @@ module SharedNote end step 'I should see no notes at all' do - expect(page).to_not have_css('.note') + expect(page).not_to have_css('.note') end # Markdown diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index ea5f9580308..b3411c03118 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -95,7 +95,7 @@ module SharedProject step 'I should see project settings' do expect(current_path).to eq edit_namespace_project_path(@project.namespace, @project) expect(page).to have_content("Project name") - expect(page).to have_content("Features:") + expect(page).to have_content("Features") end def current_project @@ -230,7 +230,7 @@ module SharedProject step 'project "Shop" has CI build' do project = Project.find_by(name: "Shop") - create :ci_commit, project: project, sha: project.commit.sha, ref: 'master', status: 'skipped' + create :ci_pipeline, project: project, sha: project.commit.sha, ref: 'master', status: 'skipped' end step 'I should see last commit with CI status' do diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index 4fc2ece79ff..bfee8793301 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -8,12 +8,8 @@ module SharedProjectTab ensure_active_main_tab('Project') end - step 'the active main tab should be Files' do - ensure_active_main_tab('Files') - end - - step 'the active main tab should be Commits' do - ensure_active_main_tab('Commits') + step 'the active main tab should be Code' do + ensure_active_main_tab('Code') end step 'the active main tab should be Graphs' do @@ -41,9 +37,7 @@ module SharedProjectTab end step 'the active main tab should be Settings' do - page.within '.nav-sidebar' do - expect(page).to have_content('Go to project') - end + expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 0) end step 'the active main tab should be Activity' do @@ -53,4 +47,12 @@ module SharedProjectTab step 'the active sub tab should be Network' do ensure_active_sub_tab('Network') end + + step 'the active sub tab should be Files' do + ensure_active_sub_tab('Files') + end + + step 'the active sub tab should be Commits' do + ensure_active_sub_tab('Commits') + end end diff --git a/features/steps/shared/shortcuts.rb b/features/steps/shared/shortcuts.rb index bbb7afec0ad..a75a8474d26 100644 --- a/features/steps/shared/shortcuts.rb +++ b/features/steps/shared/shortcuts.rb @@ -1,4 +1,4 @@ -module SharedActiveTab +module SharedShortcuts include Spinach::DSL step 'I press "g" and "p"' do diff --git a/features/steps/shared/sidebar_active_tab.rb b/features/steps/shared/sidebar_active_tab.rb new file mode 100644 index 00000000000..5c47238777f --- /dev/null +++ b/features/steps/shared/sidebar_active_tab.rb @@ -0,0 +1,35 @@ +module SharedSidebarActiveTab + include Spinach::DSL + + step 'the active main tab should be Help' do + ensure_active_main_tab('Help') + end + + step 'no other main tabs should be active' do + expect(page).to have_selector('.nav-sidebar > li.active', count: 1) + end + + def ensure_active_main_tab(content) + expect(find('.nav-sidebar li.active')).to have_content(content) + end + + step 'the active main tab should be Home' do + ensure_active_main_tab('Projects') + end + + step 'the active main tab should be Projects' do + ensure_active_main_tab('Projects') + end + + step 'the active main tab should be Issues' do + ensure_active_main_tab('Issues') + end + + step 'the active main tab should be Merge Requests' do + ensure_active_main_tab('Merge Requests') + end + + step 'the active main tab should be Help' do + ensure_active_main_tab('Help') + end +end diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb index 023032e679f..19366b11071 100644 --- a/features/steps/snippets/snippets.rb +++ b/features/steps/snippets/snippets.rb @@ -14,12 +14,12 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps step 'I click link "Edit"' do page.within ".detail-page-header" do - click_link "Edit" + first(:link, "Edit").click end end step 'I click link "Delete"' do - click_link "Delete" + first(:link, "Delete").click end step 'I submit new snippet "Personal snippet three"' do diff --git a/features/steps/user.rb b/features/steps/user.rb index b1d088f07f9..59385a6ab59 100644 --- a/features/steps/user.rb +++ b/features/steps/user.rb @@ -34,7 +34,7 @@ class Spinach::Features::User < Spinach::FeatureSteps end step 'I should see contributions calendar' do - expect(page).to have_css('.cal-heatmap-container') + expect(page).to have_css('.js-contrib-calendar') end def contributed_project diff --git a/features/support/env.rb b/features/support/env.rb index 357d164d87f..edc08cf0986 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -16,6 +16,11 @@ require_relative 'capybara' require_relative 'db_cleaner' require_relative 'rerun' +if ENV['CI'] + require 'knapsack' + Knapsack::Adapters::RSpecAdapter.bind +end + %w(select2_helper test_env repo_helpers).each do |f| require Rails.root.join('spec', 'support', f) end diff --git a/generator_templates/active_record/migration/create_table_migration.rb b/generator_templates/active_record/migration/create_table_migration.rb new file mode 100644 index 00000000000..27acc75dcc4 --- /dev/null +++ b/generator_templates/active_record/migration/create_table_migration.rb @@ -0,0 +1,35 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class <%= migration_class_name %> < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + create_table :<%= table_name %> do |t| +<% attributes.each do |attribute| -%> +<% if attribute.password_digest? -%> + t.string :password_digest<%= attribute.inject_options %> +<% else -%> + t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %> +<% end -%> +<% end -%> +<% if options[:timestamps] %> + t.timestamps null: false +<% end -%> + end +<% attributes_with_index.each do |attribute| -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> +<% end -%> + end +end diff --git a/generator_templates/active_record/migration/migration.rb b/generator_templates/active_record/migration/migration.rb new file mode 100644 index 00000000000..06bdea11367 --- /dev/null +++ b/generator_templates/active_record/migration/migration.rb @@ -0,0 +1,55 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class <%= migration_class_name %> < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + +<%- if migration_action == 'add' -%> + def change +<% attributes.each do |attribute| -%> + <%- if attribute.reference? -%> + add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %> + <%- else -%> + add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> + <%- if attribute.has_index? -%> + add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end -%> + <%- end -%> +<%- end -%> + end +<%- elsif migration_action == 'join' -%> + def change + create_join_table :<%= join_tables.first %>, :<%= join_tables.second %> do |t| + <%- attributes.each do |attribute| -%> + <%= '# ' unless attribute.has_index? -%>t.index <%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end -%> + end + end +<%- else -%> + def change +<% attributes.each do |attribute| -%> +<%- if migration_action -%> + <%- if attribute.reference? -%> + remove_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %> + <%- else -%> + <%- if attribute.has_index? -%> + remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end -%> + remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %> + <%- end -%> +<%- end -%> +<%- end -%> + end +<%- end -%> +end diff --git a/lib/api/api.rb b/lib/api/api.rb index cc1004f8005..6cd909f6115 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -1,5 +1,3 @@ -Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file} - module API class API < Grape::API include APIGuard @@ -25,38 +23,41 @@ module API format :json content_type :txt, "text/plain" - helpers Helpers - - mount Groups - mount GroupMembers - mount Users - mount Projects - mount Repositories - mount Issues - mount Milestones - mount Session - mount MergeRequests - mount Notes - mount Internal - mount SystemHooks - mount ProjectSnippets - mount ProjectMembers - mount DeployKeys - mount ProjectHooks - mount Services - mount Files - mount Commits - mount CommitStatus - mount Namespaces - mount Branches - mount Labels - mount Settings - mount Keys - mount Tags - mount Triggers - mount Builds - mount Variables - mount Runners - mount Licenses + # Ensure the namespace is right, otherwise we might load Grape::API::Helpers + helpers ::API::Helpers + + mount ::API::Groups + mount ::API::GroupMembers + mount ::API::Users + mount ::API::Projects + mount ::API::Repositories + mount ::API::Issues + mount ::API::Milestones + mount ::API::Session + mount ::API::MergeRequests + mount ::API::Notes + mount ::API::Internal + mount ::API::SystemHooks + mount ::API::ProjectSnippets + mount ::API::ProjectMembers + mount ::API::DeployKeys + mount ::API::ProjectHooks + mount ::API::Services + mount ::API::Files + mount ::API::Commits + mount ::API::CommitStatuses + mount ::API::Namespaces + mount ::API::Branches + mount ::API::Labels + mount ::API::Settings + mount ::API::Keys + mount ::API::Tags + mount ::API::Triggers + mount ::API::Builds + mount ::API::Variables + mount ::API::Runners + mount ::API::Licenses + mount ::API::Subscriptions + mount ::API::Gitignores end end diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index b9994fcefda..7e67edb203a 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -2,171 +2,175 @@ require 'rack/oauth2' -module APIGuard - extend ActiveSupport::Concern +module API + module APIGuard + extend ActiveSupport::Concern - included do |base| - # OAuth2 Resource Server Authentication - use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| - # The authenticator only fetches the raw token string + included do |base| + # OAuth2 Resource Server Authentication + use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request| + # The authenticator only fetches the raw token string - # Must yield access token to store it in the env - request.access_token - end + # Must yield access token to store it in the env + request.access_token + end - helpers HelperMethods + helpers HelperMethods - install_error_responders(base) - end + install_error_responders(base) + end - # Helper Methods for Grape Endpoint - module HelperMethods - # Invokes the doorkeeper guard. - # - # If token is presented and valid, then it sets @current_user. - # - # If the token does not have sufficient scopes to cover the requred scopes, - # then it raises InsufficientScopeError. - # - # If the token is expired, then it raises ExpiredError. - # - # If the token is revoked, then it raises RevokedError. - # - # If the token is not found (nil), then it raises TokenNotFoundError. - # - # Arguments: - # - # scopes: (optional) scopes required for this guard. - # Defaults to empty array. - # - def doorkeeper_guard!(scopes: []) - if (access_token = find_access_token).nil? - raise TokenNotFoundError - - else - case validate_access_token(access_token, scopes) - when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) - when Oauth2::AccessTokenValidationService::EXPIRED - raise ExpiredError - when Oauth2::AccessTokenValidationService::REVOKED - raise RevokedError - when Oauth2::AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) + # Helper Methods for Grape Endpoint + module HelperMethods + # Invokes the doorkeeper guard. + # + # If token is presented and valid, then it sets @current_user. + # + # If the token does not have sufficient scopes to cover the requred scopes, + # then it raises InsufficientScopeError. + # + # If the token is expired, then it raises ExpiredError. + # + # If the token is revoked, then it raises RevokedError. + # + # If the token is not found (nil), then it raises TokenNotFoundError. + # + # Arguments: + # + # scopes: (optional) scopes required for this guard. + # Defaults to empty array. + # + def doorkeeper_guard!(scopes: []) + if (access_token = find_access_token).nil? + raise TokenNotFoundError + + else + case validate_access_token(access_token, scopes) + when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + when Oauth2::AccessTokenValidationService::EXPIRED + raise ExpiredError + when Oauth2::AccessTokenValidationService::REVOKED + raise RevokedError + when Oauth2::AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) + end end end - end - def doorkeeper_guard(scopes: []) - if access_token = find_access_token - case validate_access_token(access_token, scopes) - when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) + def doorkeeper_guard(scopes: []) + if access_token = find_access_token + case validate_access_token(access_token, scopes) + when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) - when Oauth2::AccessTokenValidationService::EXPIRED - raise ExpiredError + when Oauth2::AccessTokenValidationService::EXPIRED + raise ExpiredError - when Oauth2::AccessTokenValidationService::REVOKED - raise RevokedError + when Oauth2::AccessTokenValidationService::REVOKED + raise RevokedError - when Oauth2::AccessTokenValidationService::VALID - @current_user = User.find(access_token.resource_owner_id) + when Oauth2::AccessTokenValidationService::VALID + @current_user = User.find(access_token.resource_owner_id) + end end end - end - def current_user - @current_user - end + def current_user + @current_user + end - private - def find_access_token - @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) - end + private - def doorkeeper_request - @doorkeeper_request ||= ActionDispatch::Request.new(env) - end + def find_access_token + @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) + end - def validate_access_token(access_token, scopes) - Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) - end - end + def doorkeeper_request + @doorkeeper_request ||= ActionDispatch::Request.new(env) + end - module ClassMethods - # Installs the doorkeeper guard on the whole Grape API endpoint. - # - # Arguments: - # - # scopes: (optional) scopes required for this guard. - # Defaults to empty array. - # - def guard_all!(scopes: []) - before do - guard! scopes: scopes + def validate_access_token(access_token, scopes) + Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes) end end - private - def install_error_responders(base) - error_classes = [ MissingTokenError, TokenNotFoundError, - ExpiredError, RevokedError, InsufficientScopeError] + module ClassMethods + # Installs the doorkeeper guard on the whole Grape API endpoint. + # + # Arguments: + # + # scopes: (optional) scopes required for this guard. + # Defaults to empty array. + # + def guard_all!(scopes: []) + before do + guard! scopes: scopes + end + end - base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler - end + private - def oauth2_bearer_token_error_handler - Proc.new do |e| - response = - case e - when MissingTokenError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new - - when TokenNotFoundError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Bad Access Token.") - - when ExpiredError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Token is expired. You can either do re-authorization or token refresh.") - - when RevokedError - Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( - :invalid_token, - "Token was revoked. You have to re-authorize from the user.") - - when InsufficientScopeError - # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) - # does not include WWW-Authenticate header, which breaks the standard. - Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( - :insufficient_scope, - Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope], - { scope: e.scopes }) - end + def install_error_responders(base) + error_classes = [ MissingTokenError, TokenNotFoundError, + ExpiredError, RevokedError, InsufficientScopeError] - response.finish + base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler + end + + def oauth2_bearer_token_error_handler + Proc.new do |e| + response = + case e + when MissingTokenError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new + + when TokenNotFoundError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Bad Access Token.") + + when ExpiredError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Token is expired. You can either do re-authorization or token refresh.") + + when RevokedError + Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new( + :invalid_token, + "Token was revoked. You have to re-authorize from the user.") + + when InsufficientScopeError + # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) + # does not include WWW-Authenticate header, which breaks the standard. + Rack::OAuth2::Server::Resource::Bearer::Forbidden.new( + :insufficient_scope, + Rack::OAuth2::Server::Resource::ErrorMethods::DEFAULT_DESCRIPTION[:insufficient_scope], + { scope: e.scopes }) + end + + response.finish + end end end - end - # - # Exceptions - # + # + # Exceptions + # - class MissingTokenError < StandardError; end + class MissingTokenError < StandardError; end - class TokenNotFoundError < StandardError; end + class TokenNotFoundError < StandardError; end - class ExpiredError < StandardError; end + class ExpiredError < StandardError; end - class RevokedError < StandardError; end + class RevokedError < StandardError; end - class InsufficientScopeError < StandardError - attr_reader :scopes - def initialize(scopes) - @scopes = scopes + class InsufficientScopeError < StandardError + attr_reader :scopes + def initialize(scopes) + @scopes = scopes + end end end end diff --git a/lib/api/builds.rb b/lib/api/builds.rb index 2b104f90aa7..0ff8fa74a84 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -33,7 +33,7 @@ module API get ':id/repository/commits/:sha/builds' do authorize_read_builds! - commit = user_project.ci_commits.find_by_sha(params[:sha]) + commit = user_project.pipelines.find_by_sha(params[:sha]) return not_found! unless commit builds = commit.builds.order('id DESC') diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 7388ed2f4ea..323a7086890 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -2,7 +2,7 @@ require 'mime/types' module API # Project commit statuses API - class CommitStatus < Grape::API + class CommitStatuses < Grape::API resource :projects do before { authenticate! } @@ -22,8 +22,8 @@ module API not_found!('Commit') unless user_project.commit(params[:sha]) - ci_commits = user_project.ci_commits.where(sha: params[:sha]) - statuses = ::CommitStatus.where(commit: ci_commits) + pipelines = user_project.pipelines.where(sha: params[:sha]) + statuses = ::CommitStatus.where(pipeline: pipelines) statuses = statuses.latest unless parse_boolean(params[:all]) statuses = statuses.where(ref: params[:ref]) if params[:ref].present? statuses = statuses.where(stage: params[:stage]) if params[:stage].present? @@ -50,7 +50,7 @@ module API commit = @project.commit(params[:sha]) not_found! 'Commit' unless commit - # Since the CommitStatus is attached to Ci::Commit (in the future Pipeline) + # Since the CommitStatus is attached to Ci::Pipeline (in the future Pipeline) # We need to always have the pipeline object # To have a valid pipeline object that can be attached to specific MR # Other CI service needs to send `ref` @@ -64,11 +64,11 @@ module API ref = branches.first end - ci_commit = @project.ensure_ci_commit(commit.sha, ref) + pipeline = @project.ensure_pipeline(commit.sha, ref) name = params[:name] || params[:context] - status = GenericCommitStatus.running_or_pending.find_by(commit: ci_commit, name: name, ref: params[:ref]) - status ||= GenericCommitStatus.new(project: @project, commit: ci_commit, user: current_user) + status = GenericCommitStatus.running_or_pending.find_by(pipeline: pipeline, name: name, ref: params[:ref]) + status ||= GenericCommitStatus.new(project: @project, pipeline: pipeline, user: current_user) status.update(attrs) case params[:state].to_s diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 93a3a5ce089..4a11c8e3620 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -107,6 +107,8 @@ module API break if opts[:line_code] end + + opts[:type] = LegacyDiffNote.name if opts[:line_code] end note = ::Notes::CreateService.new(user_project, current_user, opts).execute diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 716ca6f7ed9..14370ac218d 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -30,7 +30,7 @@ module API expose :identities, using: Entities::Identity expose :can_create_group?, as: :can_create_group expose :can_create_project?, as: :can_create_project - expose :two_factor_enabled + expose :two_factor_enabled?, as: :two_factor_enabled expose :external end @@ -66,7 +66,8 @@ module API expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :name, :name_with_namespace expose :path, :path_with_namespace - expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :created_at, :last_activity_at + expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :container_registry_enabled + expose :created_at, :last_activity_at expose :shared_runners_enabled expose :creator_id expose :namespace @@ -174,11 +175,18 @@ module API expose :subscribed do |issue, options| issue.subscribed?(options[:current_user]) end + expose :user_notes_count + expose :upvotes, :downvotes + end + + class ExternalIssue < Grape::Entity + expose :title + expose :id end class MergeRequest < ProjectEntity expose :target_branch, :source_branch - expose :upvotes, :downvotes + expose :upvotes, :downvotes expose :author, :assignee, using: Entities::UserBasic expose :source_project_id, :target_project_id expose :label_names, as: :labels @@ -187,10 +195,10 @@ module API expose :milestone, using: Entities::Milestone expose :merge_when_build_succeeds expose :merge_status - expose :subscribed do |merge_request, options| merge_request.subscribed?(options[:current_user]) end + expose :user_notes_count end class MergeRequestChanges < MergeRequest @@ -216,8 +224,8 @@ module API expose :system?, as: :system expose :noteable_id, :noteable_type # upvote? and downvote? are deprecated, always return false - expose :upvote?, as: :upvote - expose :downvote?, as: :downvote + expose(:upvote?) { |note| false } + expose(:downvote?) { |note| false } end class MRNote < Grape::Entity @@ -227,9 +235,9 @@ module API class CommitNote < Grape::Entity expose :note - expose(:path) { |note| note.diff_file_name } - expose(:line) { |note| note.diff_new_line } - expose(:line_type) { |note| note.diff_line_type } + expose(:path) { |note| note.diff_file_path if note.legacy_diff_note? } + expose(:line) { |note| note.diff_new_line if note.legacy_diff_note? } + expose(:line_type) { |note| note.diff_line_type if note.legacy_diff_note? } expose :author, using: Entities::UserBasic expose :created_at end @@ -307,6 +315,10 @@ module API class Label < Grape::Entity expose :name, :color, :description expose :open_issues_count, :closed_issues_count, :open_merge_requests_count + + expose :subscribed do |label, options| + label.subscribed?(options[:current_user]) + end end class Compare < Grape::Entity @@ -344,6 +356,7 @@ module API expose :signin_enabled expose :gravatar_enabled expose :sign_in_text + expose :after_sign_up_text expose :created_at expose :updated_at expose :home_page_url @@ -357,6 +370,7 @@ module API expose :restricted_signup_domains expose :user_oauth_applications expose :after_sign_out_path + expose :container_registry_token_expire_delay end class Release < Grape::Entity @@ -403,6 +417,7 @@ module API class RunnerDetails < Runner expose :tag_list + expose :run_untagged expose :version, :revision, :platform, :architecture expose :contacted_at expose :token, if: lambda { |runner, options| options[:current_user].is_admin? || !runner.is_shared? } @@ -451,5 +466,13 @@ module API expose(:limitations) { |license| license.meta['limitations'] } expose :content end + + class GitignoresList < Grape::Entity + expose :name + end + + class Gitignore < Grape::Entity + expose :name, :content + end end end diff --git a/lib/api/gitignores.rb b/lib/api/gitignores.rb new file mode 100644 index 00000000000..270c9501dd2 --- /dev/null +++ b/lib/api/gitignores.rb @@ -0,0 +1,29 @@ +module API + class Gitignores < Grape::API + + # Get the list of the available gitignore templates + # + # Example Request: + # GET /gitignores + get 'gitignores' do + present Gitlab::Gitignore.all, with: Entities::GitignoresList + end + + # Get the text for a specific gitignore + # + # Parameters: + # name (required) - The name of a license + # + # Example Request: + # GET /gitignores/Elixir + # + get 'gitignores/:name' do + required_attributes! [:name] + + gitignore = Gitlab::Gitignore.find(params[:name]) + not_found!('.gitignore') unless gitignore + + present gitignore, with: Entities::Gitignore + end + end +end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 91e420832f3..9d8b8d737a9 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -95,8 +95,7 @@ module API # GET /groups/:id/projects get ":id/projects" do group = find_group(params[:id]) - projects = group.projects - projects = filter_projects(projects) + projects = GroupProjectsFinder.new(group).execute(current_user) projects = paginate projects present projects, with: Entities::Project end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 40c967453fb..de5959e3aae 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -2,7 +2,7 @@ module API module Helpers PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN" PRIVATE_TOKEN_PARAM = :private_token - SUDO_HEADER ="HTTP_SUDO" + SUDO_HEADER = "HTTP_SUDO" SUDO_PARAM = :sudo def parse_boolean(value) @@ -29,7 +29,7 @@ module API @current_user end - def sudo_identifier() + def sudo_identifier identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] # Regex for integers @@ -95,6 +95,17 @@ module API end end + def find_project_label(id) + label = user_project.labels.find_by_id(id) || user_project.labels.find_by_title(id) + label || not_found!('Label') + end + + def find_project_issue(id) + issue = user_project.issues.find(id) + not_found! unless can?(current_user, :read_issue, issue) + issue + end + def paginate(relation) relation.page(params[:page]).per(params[:per_page].to_i).tap do |data| add_pagination_headers(data) @@ -397,5 +408,23 @@ module API error!(errors[:access_level], 422) if errors[:access_level].any? not_found!(errors) end + + def send_git_blob(repository, blob) + env['api.format'] = :txt + content_type 'text/plain' + header(*Gitlab::Workhorse.send_git_blob(repository, blob)) + end + + def send_git_archive(repository, ref:, format:) + header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format)) + end + + def issue_entity(project) + if project.has_external_issue_tracker? + Entities::ExternalIssue + else + Entities::Issue + end + end end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 40928749481..4c43257c48a 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -51,7 +51,7 @@ module API # GET /issues?labels=foo,bar # GET /issues?labels=foo,bar&state=opened get do - issues = current_user.issues + issues = current_user.issues.inc_notes_with_associations issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues.reorder(issuable_order_by => issuable_sort) @@ -82,7 +82,7 @@ module API # GET /projects/:id/issues?milestone=1.0.0&state=closed # GET /issues?iid=42 get ":id/issues" do - issues = user_project.issues.visible_to_user(current_user) + issues = user_project.issues.inc_notes_with_associations.visible_to_user(current_user) issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil? @@ -103,8 +103,7 @@ module API # Example Request: # GET /projects/:id/issues/:issue_id get ":id/issues/:issue_id" do - @issue = user_project.issues.find(params[:issue_id]) - not_found! unless can?(current_user, :read_issue, @issue) + @issue = find_project_issue(params[:issue_id]) present @issue, with: Entities::Issue, current_user: current_user end @@ -234,42 +233,6 @@ module API authorize!(:destroy_issue, issue) issue.destroy end - - # Subscribes to a project issue - # - # Parameters: - # id (required) - The ID of a project - # issue_id (required) - The ID of a project issue - # Example Request: - # POST /projects/:id/issues/:issue_id/subscription - post ':id/issues/:issue_id/subscription' do - issue = user_project.issues.find(params[:issue_id]) - - if issue.subscribed?(current_user) - not_modified! - else - issue.toggle_subscription(current_user) - present issue, with: Entities::Issue, current_user: current_user - end - end - - # Unsubscribes from a project issue - # - # Parameters: - # id (required) - The ID of a project - # issue_id (required) - The ID of a project issue - # Example Request: - # DELETE /projects/:id/issues/:issue_id/subscription - delete ':id/issues/:issue_id/subscription' do - issue = user_project.issues.find(params[:issue_id]) - - if issue.subscribed?(current_user) - issue.unsubscribe(current_user) - present issue, with: Entities::Issue, current_user: current_user - else - not_modified! - end - end end end end diff --git a/lib/api/labels.rb b/lib/api/labels.rb index 4af6bef0fa7..c806829d69e 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -11,7 +11,7 @@ module API # Example Request: # GET /projects/:id/labels get ':id/labels' do - present user_project.labels, with: Entities::Label + present user_project.labels, with: Entities::Label, current_user: current_user end # Creates a new label @@ -36,7 +36,7 @@ module API label = user_project.labels.create(attrs) if label.valid? - present label, with: Entities::Label + present label, with: Entities::Label, current_user: current_user else render_validation_error!(label) end @@ -90,7 +90,7 @@ module API attrs[:name] = attrs.delete(:new_name) if attrs.key?(:new_name) if label.update(attrs) - present label, with: Entities::Label + present label, with: Entities::Label, current_user: current_user else render_validation_error!(label) end diff --git a/lib/api/licenses.rb b/lib/api/licenses.rb index 187d2c04703..be0e113fbcb 100644 --- a/lib/api/licenses.rb +++ b/lib/api/licenses.rb @@ -2,15 +2,15 @@ module API # Licenses API class Licenses < Grape::API PROJECT_TEMPLATE_REGEX = - /[\<\{\[] - (project|description| - one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here - [\>\}\]]/xi.freeze + /[\<\{\[] + (project|description| + one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here + [\>\}\]]/xi.freeze YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze FULLNAME_TEMPLATE_REGEX = - /[\<\{\[] - (fullname|name\sof\s(author|copyright\sowner)) - [\>\}\]]/xi.freeze + /[\<\{\[] + (fullname|name\sof\s(author|copyright\sowner)) + [\>\}\]]/xi.freeze # Get the list of the available license templates # diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 7e78609ecb9..0e94efd4acd 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -41,7 +41,7 @@ module API # get ":id/merge_requests" do authorize! :read_merge_request, user_project - merge_requests = user_project.merge_requests + merge_requests = user_project.merge_requests.inc_notes_with_associations unless params[:iid].nil? merge_requests = filter_by_iid(merge_requests, params[:iid]) @@ -218,6 +218,7 @@ module API # merge_commit_message (optional) - Custom merge commit message # should_remove_source_branch (optional) - When true, the source branch will be deleted if possible # merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds + # sha (optional) - When present, must have the HEAD SHA of the source branch # Example: # PUT /projects/:id/merge_requests/:merge_request_id/merge # @@ -227,18 +228,21 @@ module API # Merge request can not be merged # because user dont have permissions to push into target branch unauthorized! unless merge_request.can_be_merged_by?(current_user) - not_allowed! if !merge_request.open? || merge_request.work_in_progress? - merge_request.check_if_can_be_merged + not_allowed! unless merge_request.mergeable_state? - render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged? + render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable? + + if params[:sha] && merge_request.source_sha != params[:sha] + render_api_error!("SHA does not match HEAD of source branch: #{merge_request.source_sha}", 409) + end merge_params = { commit_message: params[:merge_commit_message], should_remove_source_branch: params[:should_remove_source_branch] } - if parse_boolean(params[:merge_when_build_succeeds]) && merge_request.ci_commit && merge_request.ci_commit.active? + if parse_boolean(params[:merge_when_build_succeeds]) && merge_request.pipeline && merge_request.pipeline.active? ::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params). execute(merge_request) else @@ -325,43 +329,7 @@ module API get "#{path}/closes_issues" do merge_request = user_project.merge_requests.find(params[:merge_request_id]) issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) - present paginate(issues), with: Entities::Issue, current_user: current_user - end - - # Subscribes to a merge request - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - The ID of a merge request - # Example Request: - # POST /projects/:id/issues/:merge_request_id/subscription - post "#{path}/subscription" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) - - if merge_request.subscribed?(current_user) - not_modified! - else - merge_request.toggle_subscription(current_user) - present merge_request, with: Entities::MergeRequest, current_user: current_user - end - end - - # Unsubscribes from a merge request - # - # Parameters: - # id (required) - The ID of a project - # merge_request_id (required) - The ID of a merge request - # Example Request: - # DELETE /projects/:id/merge_requests/:merge_request_id/subscription - delete "#{path}/subscription" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) - - if merge_request.subscribed?(current_user) - merge_request.unsubscribe(current_user) - present merge_request, with: Entities::MergeRequest, current_user: current_user - else - not_modified! - end + present paginate(issues), with: issue_entity(user_project), current_user: current_user end end end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 71a53e6f0d6..d4fcfd3d4d3 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -19,20 +19,24 @@ module API # GET /projects/:id/issues/:noteable_id/notes # GET /projects/:id/snippets/:noteable_id/notes get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do - @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) - - # We exclude notes that are cross-references and that cannot be viewed - # by the current user. By doing this exclusion at this level and not - # at the DB query level (which we cannot in that case), the current - # page can have less elements than :per_page even if - # there's more than one page. - notes = - # paginate() only works with a relation. This could lead to a - # mismatch between the pagination headers info and the actual notes - # array returned, but this is really a edge-case. - paginate(@noteable.notes). - reject { |n| n.cross_reference_not_visible_for?(current_user) } - present notes, with: Entities::Note + @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) + + if can?(current_user, noteable_read_ability_name(@noteable), @noteable) + # We exclude notes that are cross-references and that cannot be viewed + # by the current user. By doing this exclusion at this level and not + # at the DB query level (which we cannot in that case), the current + # page can have less elements than :per_page even if + # there's more than one page. + notes = + # paginate() only works with a relation. This could lead to a + # mismatch between the pagination headers info and the actual notes + # array returned, but this is really a edge-case. + paginate(@noteable.notes). + reject { |n| n.cross_reference_not_visible_for?(current_user) } + present notes, with: Entities::Note + else + not_found!("Notes") + end end # Get a single +noteable+ note @@ -45,13 +49,14 @@ module API # GET /projects/:id/issues/:noteable_id/notes/:note_id # GET /projects/:id/snippets/:noteable_id/notes/:note_id get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do - @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) + @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym]) @note = @noteable.notes.find(params[:note_id]) + can_read_note = can?(current_user, noteable_read_ability_name(@noteable), @noteable) && !@note.cross_reference_not_visible_for?(current_user) - if @note.cross_reference_not_visible_for?(current_user) - not_found!("Note") - else + if can_read_note present @note, with: Entities::Note + else + not_found!("Note") end end @@ -136,5 +141,11 @@ module API end end end + + helpers do + def noteable_read_ability_name(noteable) + "read_#{noteable.class.to_s.underscore.downcase}".to_sym + end + end end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index cc2c7a0c503..5a22d14988f 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -44,7 +44,7 @@ module API # Example Request: # GET /projects/starred get '/starred' do - @projects = current_user.starred_projects + @projects = current_user.viewable_starred_projects @projects = filter_projects(@projects) @projects = paginate @projects present @projects, with: Entities::Project @@ -94,6 +94,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # container_registry_enabled (optional) # shared_runners_enabled (optional) # namespace_id (optional) - defaults to user namespace # public (optional) - if true same as setting visibility_level = 20 @@ -112,6 +113,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, + :container_registry_enabled, :shared_runners_enabled, :namespace_id, :public, @@ -143,6 +145,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # container_registry_enabled (optional) # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) @@ -206,6 +209,7 @@ module API # builds_enabled (optional) # wiki_enabled (optional) # snippets_enabled (optional) + # container_registry_enabled (optional) # shared_runners_enabled (optional) # public (optional) - if true same as setting visibility_level = 20 # visibility_level (optional) - visibility level of a project @@ -222,6 +226,7 @@ module API :builds_enabled, :wiki_enabled, :snippets_enabled, + :container_registry_enabled, :shared_runners_enabled, :public, :visibility_level, diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 62161aadb9a..f55aceed92c 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -56,8 +56,7 @@ module API blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath]) not_found! "File" unless blob - content_type 'text/plain' - header *Gitlab::Workhorse.send_git_blob(repo, blob) + send_git_blob repo, blob end # Get a raw blob contents by blob sha @@ -80,10 +79,7 @@ module API not_found! 'Blob' unless blob - env['api.format'] = :txt - - content_type blob.mime_type - header *Gitlab::Workhorse.send_git_blob(repo, blob) + send_git_blob repo, blob end # Get a an archive of the repository @@ -98,7 +94,7 @@ module API authorize! :download_code, user_project begin - header *Gitlab::Workhorse.send_git_archive(user_project, params[:sha], params[:format]) + send_git_archive user_project.repository, ref: params[:sha], format: params[:format] rescue not_found!('File') end diff --git a/lib/api/runners.rb b/lib/api/runners.rb index 8ec91485b26..4faba9dc87b 100644 --- a/lib/api/runners.rb +++ b/lib/api/runners.rb @@ -49,7 +49,7 @@ module API runner = get_runner(params[:id]) authenticate_update_runner!(runner) - attrs = attributes_for_keys [:description, :active, :tag_list] + attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged] if runner.update(attrs) present runner, with: Entities::RunnerDetails, current_user: current_user else diff --git a/lib/api/session.rb b/lib/api/session.rb index cc646895914..56e69b2366f 100644 --- a/lib/api/session.rb +++ b/lib/api/session.rb @@ -11,8 +11,7 @@ module API # Example Request: # POST /session post "/session" do - auth = Gitlab::Auth.new - user = auth.find(params[:email] || params[:login], params[:password]) + user = Gitlab::Auth.find_in_gitlab_or_ldap(params[:email] || params[:login], params[:password]) return unauthorized! unless user present user, with: Entities::UserLogin diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb new file mode 100644 index 00000000000..c49e2a21b82 --- /dev/null +++ b/lib/api/subscriptions.rb @@ -0,0 +1,60 @@ +module API + class Subscriptions < Grape::API + before { authenticate! } + + subscribable_types = { + 'merge_request' => proc { |id| user_project.merge_requests.find(id) }, + 'merge_requests' => proc { |id| user_project.merge_requests.find(id) }, + 'issues' => proc { |id| find_project_issue(id) }, + 'labels' => proc { |id| find_project_label(id) }, + } + + resource :projects do + subscribable_types.each do |type, finder| + type_singularized = type.singularize + type_id_str = :"#{type_singularized}_id" + entity_class = Entities.const_get(type_singularized.camelcase) + + # Subscribe to a resource + # + # Parameters: + # id (required) - The ID of a project + # subscribable_id (required) - The ID of a resource + # Example Request: + # POST /projects/:id/labels/:subscribable_id/subscription + # POST /projects/:id/issues/:subscribable_id/subscription + # POST /projects/:id/merge_requests/:subscribable_id/subscription + post ":id/#{type}/:#{type_id_str}/subscription" do + resource = instance_exec(params[type_id_str], &finder) + + if resource.subscribed?(current_user) + not_modified! + else + resource.subscribe(current_user) + present resource, with: entity_class, current_user: current_user + end + end + + # Unsubscribe from a resource + # + # Parameters: + # id (required) - The ID of a project + # subscribable_id (required) - The ID of a resource + # Example Request: + # DELETE /projects/:id/labels/:subscribable_id/subscription + # DELETE /projects/:id/issues/:subscribable_id/subscription + # DELETE /projects/:id/merge_requests/:subscribable_id/subscription + delete ":id/#{type}/:#{type_id_str}/subscription" do + resource = instance_exec(params[type_id_str], &finder) + + if !resource.subscribed?(current_user) + not_modified! + else + resource.unsubscribe(current_user) + present resource, with: entity_class, current_user: current_user + end + end + end + end + end +end diff --git a/lib/api/users.rb b/lib/api/users.rb index ea6fa2dc8a8..8a376d3c2a3 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -76,7 +76,7 @@ module API required_attributes! [:email, :password, :name, :username] attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external] admin = attrs.delete(:admin) - confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i)) + confirm = !(attrs.delete(:confirm) =~ /(false|f|no|0)$/i) user = User.build_user(attrs) user.admin = admin unless admin.nil? user.skip_confirmation! unless confirm diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb deleted file mode 100644 index b1aecc2e671..00000000000 --- a/lib/award_emoji.rb +++ /dev/null @@ -1,84 +0,0 @@ -class AwardEmoji - CATEGORIES = { - other: "Other", - objects: "Objects", - places: "Places", - travel_places: "Travel", - emoticons: "Emoticons", - objects_symbols: "Symbols", - nature: "Nature", - celebration: "Celebration", - people: "People", - activity: "Activity", - flags: "Flags", - food_drink: "Food" - }.with_indifferent_access - - CATEGORY_ALIASES = { - symbols: "objects_symbols", - foods: "food_drink", - travel: "travel_places" - }.with_indifferent_access - - def self.normilize_emoji_name(name) - aliases[name] || name - end - - def self.emoji_by_category - unless @emoji_by_category - @emoji_by_category = Hash.new { |h, key| h[key] = [] } - - emojis.each do |emoji_name, data| - data["name"] = emoji_name - - # Skip Fitzpatrick(tone) modifiers - next if data["category"] == "modifier" - - category = CATEGORY_ALIASES[data["category"]] || data["category"] - - @emoji_by_category[category] << data - end - - @emoji_by_category = @emoji_by_category.sort.to_h - end - - @emoji_by_category - end - - def self.emojis - @emojis ||= begin - json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' ) - JSON.parse(File.read(json_path)) - end - end - - def self.unicode - @unicode ||= emojis.map {|key, value| { key => emojis[key]["unicode"] } }.inject(:merge!) - end - - def self.aliases - @aliases ||= begin - json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' ) - JSON.parse(File.read(json_path)) - end - end - - # Returns an Array of Emoji names and their asset URLs. - def self.urls - @urls ||= begin - path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') - prefix = Gitlab::Application.config.assets.prefix - digest = Gitlab::Application.config.assets.digest - - JSON.parse(File.read(path)).map do |hash| - if digest - fname = "#{hash['unicode']}-#{hash['digest']}" - else - fname = hash['unicode'] - end - - { name: hash['name'], path: "#{prefix}/#{fname}.png" } - end - end - end -end diff --git a/lib/backup/database.rb b/lib/backup/database.rb index 67b2a64bd10..22319ec6623 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -86,9 +86,9 @@ module Backup def report_success(success) if success - $progress.puts '[DONE]'.green + $progress.puts '[DONE]'.color(:green) else - $progress.puts '[FAILED]'.red + $progress.puts '[FAILED]'.color(:red) end end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 4962f5e53ce..2ff3e3bdfb0 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -1,5 +1,8 @@ module Backup class Manager + ARCHIVES_TO_BACKUP = %w[uploads builds artifacts lfs registry] + FOLDERS_TO_BACKUP = %w[repositories db] + def pack # Make sure there is a connection ActiveRecord::Base.connection.reconnect! @@ -24,9 +27,9 @@ module Backup # Set file permissions on open to prevent chmod races. tar_system_options = {out: [tar_file, 'w', Gitlab.config.backup.archive_permissions]} if Kernel.system('tar', '-cf', '-', *backup_contents, tar_system_options) - $progress.puts "done".green + $progress.puts "done".color(:green) else - puts "creating archive #{tar_file} failed".red + puts "creating archive #{tar_file} failed".color(:red) abort 'Backup failed' end @@ -35,24 +38,22 @@ module Backup end def upload(tar_file) - remote_directory = Gitlab.config.backup.upload.remote_directory $progress.print "Uploading backup archive to remote storage #{remote_directory} ... " connection_settings = Gitlab.config.backup.upload.connection if connection_settings.blank? - $progress.puts "skipped".yellow + $progress.puts "skipped".color(:yellow) return end - connection = ::Fog::Storage.new(connection_settings) - directory = connection.directories.get(remote_directory) + directory = connect_to_remote_directory(connection_settings) if directory.files.create(key: tar_file, body: File.open(tar_file), public: false, multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size, encryption: Gitlab.config.backup.upload.encryption) - $progress.puts "done".green + $progress.puts "done".color(:green) else - puts "uploading backup to #{remote_directory} failed".red + puts "uploading backup to #{remote_directory} failed".color(:red) abort 'Backup failed' end end @@ -64,9 +65,9 @@ module Backup next unless File.exist?(File.join(Gitlab.config.backup.path, dir)) if FileUtils.rm_rf(File.join(Gitlab.config.backup.path, dir)) - $progress.puts "done".green + $progress.puts "done".color(:green) else - puts "deleting tmp directory '#{dir}' failed".red + puts "deleting tmp directory '#{dir}' failed".color(:red) abort 'Backup failed' end end @@ -92,9 +93,9 @@ module Backup end end - $progress.puts "done. (#{removed} removed)".green + $progress.puts "done. (#{removed} removed)".color(:green) else - $progress.puts "skipping".yellow + $progress.puts "skipping".color(:yellow) end end @@ -121,20 +122,20 @@ module Backup $progress.print "Unpacking backup ... " unless Kernel.system(*%W(tar -xf #{tar_file})) - puts "unpacking backup failed".red + puts "unpacking backup failed".color(:red) exit 1 else - $progress.puts "done".green + $progress.puts "done".color(:green) end ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0 # restoring mismatching backups can lead to unexpected problems if settings[:gitlab_version] != Gitlab::VERSION - puts "GitLab version mismatch:".red - puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".red - puts " Please switch to the following version and try again:".red - puts " version: #{settings[:gitlab_version]}".red + puts "GitLab version mismatch:".color(:red) + puts " Your current GitLab version (#{Gitlab::VERSION}) differs from the GitLab version in the backup!".color(:red) + puts " Please switch to the following version and try again:".color(:red) + puts " version: #{settings[:gitlab_version]}".color(:red) puts puts "Hint: git checkout v#{settings[:gitlab_version]}" exit 1 @@ -147,21 +148,44 @@ module Backup end def skipped?(item) - settings[:skipped] && settings[:skipped].include?(item) + settings[:skipped] && settings[:skipped].include?(item) || disabled_features.include?(item) end private + def connect_to_remote_directory(connection_settings) + connection = ::Fog::Storage.new(connection_settings) + + # We only attempt to create the directory for local backups. For AWS + # and other cloud providers, we cannot guarantee the user will have + # permission to create the bucket. + if connection.service == ::Fog::Storage::Local + connection.directories.create(key: remote_directory) + else + connection.directories.get(remote_directory) + end + end + + def remote_directory + Gitlab.config.backup.upload.remote_directory + end + def backup_contents folders_to_backup + archives_to_backup + ["backup_information.yml"] end def archives_to_backup - %w{uploads builds artifacts lfs}.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact + ARCHIVES_TO_BACKUP.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact end def folders_to_backup - %w{repositories db}.reject{ |name| skipped?(name) } + FOLDERS_TO_BACKUP.reject{ |name| skipped?(name) } + end + + def disabled_features + features = [] + features << 'registry' unless Gitlab.config.registry.enabled + features end def settings diff --git a/lib/backup/registry.rb b/lib/backup/registry.rb new file mode 100644 index 00000000000..67fe0231087 --- /dev/null +++ b/lib/backup/registry.rb @@ -0,0 +1,13 @@ +require 'backup/files' + +module Backup + class Registry < Files + def initialize + super('registry', Settings.registry.path) + end + + def create_files_dir + Dir.mkdir(app_files_dir, 0700) + end + end +end diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index a82a7e1f7bf..7b91215d50b 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -14,14 +14,14 @@ module Backup FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace if project.empty_repo? - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else cmd = %W(tar -cf #{path_to_bundle(project)} -C #{path_to_repo(project)} .) output, status = Gitlab::Popen.popen(cmd) if status.zero? - $progress.puts "[DONE]".green + $progress.puts "[DONE]".color(:green) else - puts "[FAILED]".red + puts "[FAILED]".color(:red) puts "failed: #{cmd.join(' ')}" puts output abort 'Backup failed' @@ -33,14 +33,14 @@ module Backup if File.exists?(path_to_repo(wiki)) $progress.print " * #{wiki.path_with_namespace} ... " if wiki.repository.empty? - $progress.puts " [SKIPPED]".cyan + $progress.puts " [SKIPPED]".color(:cyan) else cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all) output, status = Gitlab::Popen.popen(cmd) if status.zero? - $progress.puts " [DONE]".green + $progress.puts " [DONE]".color(:green) else - puts " [FAILED]".red + puts " [FAILED]".color(:red) puts "failed: #{cmd.join(' ')}" abort 'Backup failed' end @@ -71,9 +71,9 @@ module Backup end if system(*cmd, silent) - $progress.puts "[DONE]".green + $progress.puts "[DONE]".color(:green) else - puts "[FAILED]".red + puts "[FAILED]".color(:red) puts "failed: #{cmd.join(' ')}" abort 'Restore failed' end @@ -90,21 +90,21 @@ module Backup cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)}) if system(*cmd, silent) - $progress.puts " [DONE]".green + $progress.puts " [DONE]".color(:green) else - puts " [FAILED]".red + puts " [FAILED]".color(:red) puts "failed: #{cmd.join(' ')}" abort 'Restore failed' end end end - $progress.print 'Put GitLab hooks in repositories dirs'.yellow + $progress.print 'Put GitLab hooks in repositories dirs'.color(:yellow) cmd = "#{Gitlab.config.gitlab_shell.path}/bin/create-hooks" if system(cmd) - $progress.puts " [DONE]".green + $progress.puts " [DONE]".color(:green) else - puts " [FAILED]".red + puts " [FAILED]".color(:red) puts "failed: #{cmd}" end diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index b8962379cb5..db95d7c908b 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -18,10 +18,6 @@ module Banzai @object_sym ||= object_name.to_sym end - def self.data_reference - @data_reference ||= "data-#{object_name.dasherize}" - end - def self.object_class_title @object_title ||= object_class.name.titleize end @@ -45,10 +41,6 @@ module Banzai end end - def self.referenced_by(node) - { object_sym => LazyReference.new(object_class, node.attr(data_reference)) } - end - def object_class self.class.object_class end @@ -236,7 +228,9 @@ module Banzai if cache.key?(key) cache[key] else - cache[key] = yield + value = yield + cache[key] = value if key.present? + value end end end diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb index b469ea0f626..bbb88c979cc 100644 --- a/lib/banzai/filter/commit_range_reference_filter.rb +++ b/lib/banzai/filter/commit_range_reference_filter.rb @@ -4,6 +4,8 @@ module Banzai # # This filter supports cross-project references. class CommitRangeReferenceFilter < AbstractReferenceFilter + self.reference_type = :commit_range + def self.object_class CommitRange end @@ -14,34 +16,18 @@ module Banzai end end - def self.referenced_by(node) - project = Project.find(node.attr("data-project")) rescue nil - return unless project - - id = node.attr("data-commit-range") - range = find_object(project, id) - - return unless range - - { commit_range: range } - end - def initialize(*args) super @commit_map = {} end - def self.find_object(project, id) + def find_object(project, id) range = CommitRange.new(id, project) range.valid_commits? ? range : nil end - def find_object(*args) - self.class.find_object(*args) - end - def url_for_object(range, project) h = Gitlab::Routing.url_helpers h.namespace_project_compare_url(project.namespace, project, diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb index bd88207326c..2ce1816672b 100644 --- a/lib/banzai/filter/commit_reference_filter.rb +++ b/lib/banzai/filter/commit_reference_filter.rb @@ -4,6 +4,8 @@ module Banzai # # This filter supports cross-project references. class CommitReferenceFilter < AbstractReferenceFilter + self.reference_type = :commit + def self.object_class Commit end @@ -14,28 +16,12 @@ module Banzai end end - def self.referenced_by(node) - project = Project.find(node.attr("data-project")) rescue nil - return unless project - - id = node.attr("data-commit") - commit = find_object(project, id) - - return unless commit - - { commit: commit } - end - - def self.find_object(project, id) + def find_object(project, id) if project && project.valid_repo? project.commit(id) end end - def find_object(*args) - self.class.find_object(*args) - end - def url_for_object(commit, project) h = Gitlab::Routing.url_helpers h.namespace_project_commit_url(project.namespace, project, commit, diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb index 37344b90576..eaa702952cc 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -4,6 +4,8 @@ module Banzai # References are ignored if the project doesn't use an external issue # tracker. class ExternalIssueReferenceFilter < ReferenceFilter + self.reference_type = :external_issue + # Public: Find `JIRA-123` issue references in text # # ExternalIssueReferenceFilter.references_in(text) do |match, issue| @@ -21,18 +23,6 @@ module Banzai end end - def self.referenced_by(node) - project = Project.find(node.attr("data-project")) rescue nil - return unless project - - id = node.attr("data-external-issue") - external_issue = ExternalIssue.new(id, project) - - return unless external_issue - - { external_issue: external_issue } - end - def call # Early return if the project isn't using an external tracker return doc if project.nil? || default_issues_tracker? diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb index 38c4219518e..f73ecfc9418 100644 --- a/lib/banzai/filter/external_link_filter.rb +++ b/lib/banzai/filter/external_link_filter.rb @@ -15,6 +15,7 @@ module Banzai next if link.start_with?(internal_url) node.set_attribute('rel', 'nofollow noreferrer') + node.set_attribute('target', '_blank') end doc diff --git a/lib/banzai/filter/inline_diff_filter.rb b/lib/banzai/filter/inline_diff_filter.rb new file mode 100644 index 00000000000..beb21b19ab3 --- /dev/null +++ b/lib/banzai/filter/inline_diff_filter.rb @@ -0,0 +1,26 @@ +module Banzai + module Filter + class InlineDiffFilter < HTML::Pipeline::Filter + IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set + + def call + search_text_nodes(doc).each do |node| + next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) + + content = node.to_html + html_content = inline_diff_filter(content) + + next if content == html_content + + node.replace(html_content) + end + doc + end + + def inline_diff_filter(text) + html_content = text.gsub(/(?:\[\-(.*?)\-\]|\{\-(.*?)\-\})/, '<span class="idiff left right deletion">\1\2</span>') + html_content.gsub(/(?:\[\+(.*?)\+\]|\{\+(.*?)\+\})/, '<span class="idiff left right addition">\1\2</span>') + end + end + end +end diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb index 2732e0b5145..2496e704002 100644 --- a/lib/banzai/filter/issue_reference_filter.rb +++ b/lib/banzai/filter/issue_reference_filter.rb @@ -5,15 +5,12 @@ module Banzai # # This filter supports cross-project references. class IssueReferenceFilter < AbstractReferenceFilter + self.reference_type = :issue + def self.object_class Issue end - def self.user_can_see_reference?(user, node, context) - issue = Issue.find(node.attr('data-issue')) rescue nil - Ability.abilities.allowed?(user, :read_issue, issue) - end - def find_object(project, id) project.get_issue(id) end diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index 8488a493b55..e4d3f87d0aa 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -2,6 +2,8 @@ module Banzai module Filter # HTML filter that replaces label references with links. class LabelReferenceFilter < AbstractReferenceFilter + self.reference_type = :label + def self.object_class Label end diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb index cad38a51851..ac5216d9cfb 100644 --- a/lib/banzai/filter/merge_request_reference_filter.rb +++ b/lib/banzai/filter/merge_request_reference_filter.rb @@ -5,6 +5,8 @@ module Banzai # # This filter supports cross-project references. class MergeRequestReferenceFilter < AbstractReferenceFilter + self.reference_type = :merge_request + def self.object_class MergeRequest end diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 4cb82178024..ca686c87d97 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -2,6 +2,8 @@ module Banzai module Filter # HTML filter that replaces milestone references with links. class MilestoneReferenceFilter < AbstractReferenceFilter + self.reference_type = :milestone + def self.object_class Milestone end @@ -10,11 +12,53 @@ module Banzai project.milestones.find_by(iid: id) end - def url_for_object(issue, project) + def references_in(text, pattern = Milestone.reference_pattern) + # We'll handle here the references that follow the `reference_pattern`. + # Other patterns (for example, the link pattern) are handled by the + # default implementation. + return super(text, pattern) if pattern != Milestone.reference_pattern + + text.gsub(pattern) do |match| + milestone = find_milestone($~[:project], $~[:milestone_iid], $~[:milestone_name]) + + if milestone + yield match, milestone.iid, $~[:project], $~ + else + match + end + end + end + + def find_milestone(project_ref, milestone_id, milestone_name) + project = project_from_ref(project_ref) + return unless project + + milestone_params = milestone_params(milestone_id, milestone_name) + project.milestones.find_by(milestone_params) + end + + def milestone_params(iid, name) + if name + { name: name.tr('"', '') } + else + { iid: iid.to_i } + end + end + + def url_for_object(milestone, project) h = Gitlab::Routing.url_helpers h.namespace_project_milestone_url(project.namespace, project, milestone, only_path: context[:only_path]) end + + def object_link_text(object, matches) + if context[:project] == object.project + super + else + "#{escape_once(super)} <i>in #{escape_once(object.project.path_with_namespace)}</i>". + html_safe + end + end end end end diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb index e589b5df6ec..c753a84a20d 100644 --- a/lib/banzai/filter/redactor_filter.rb +++ b/lib/banzai/filter/redactor_filter.rb @@ -7,8 +7,11 @@ module Banzai # class RedactorFilter < HTML::Pipeline::Filter def call - Querying.css(doc, 'a.gfm').each do |node| - unless user_can_see_reference?(node) + nodes = Querying.css(doc, 'a.gfm[data-reference-type]') + visible = nodes_visible_to_user(nodes) + + nodes.each do |node| + unless visible.include?(node) # The reference should be replaced by the original text, # which is not always the same as the rendered text. text = node.attr('data-original') || node.text @@ -21,20 +24,30 @@ module Banzai private - def user_can_see_reference?(node) - if node.has_attribute?('data-reference-filter') - reference_type = node.attr('data-reference-filter') - reference_filter = Banzai::Filter.const_get(reference_type) + def nodes_visible_to_user(nodes) + per_type = Hash.new { |h, k| h[k] = [] } + visible = Set.new + + nodes.each do |node| + per_type[node.attr('data-reference-type')] << node + end + + per_type.each do |type, nodes| + parser = Banzai::ReferenceParser[type].new(project, current_user) - reference_filter.user_can_see_reference?(current_user, node, context) - else - true + visible.merge(parser.nodes_visible_to_user(current_user, nodes)) end + + visible end def current_user context[:current_user] end + + def project + context[:project] + end end end end diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 31386cf851c..2d6f34c9cd8 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -8,24 +8,8 @@ module Banzai # :project (required) - Current project, ignored if reference is cross-project. # :only_path - Generate path-only links. class ReferenceFilter < HTML::Pipeline::Filter - def self.user_can_see_reference?(user, node, context) - if node.has_attribute?('data-project') - project_id = node.attr('data-project').to_i - return true if project_id == context[:project].try(:id) - - project = Project.find(project_id) rescue nil - Ability.abilities.allowed?(user, :read_project, project) - else - true - end - end - - def self.user_can_reference?(user, node, context) - true - end - - def self.referenced_by(node) - raise NotImplementedError, "#{self} does not implement #{__method__}" + class << self + attr_accessor :reference_type end # Returns a data attribute String to attach to a reference link @@ -43,7 +27,9 @@ module Banzai # # Returns a String def data_attribute(attributes = {}) - attributes[:reference_filter] = self.class.name.demodulize + attributes = attributes.reject { |_, v| v.nil? } + + attributes[:reference_type] = self.class.reference_type attributes.delete(:original) if context[:no_original_data] attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ") end @@ -82,6 +68,8 @@ module Banzai # by `ignore_ancestor_query`. Link tags are not processed if they have a # "gfm" class or the "href" attribute is empty. def each_node + return to_enum(__method__) unless block_given? + query = %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})] | descendant-or-self::a[ not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "") @@ -92,6 +80,11 @@ module Banzai end end + # Returns an Array containing all HTML nodes. + def nodes + @nodes ||= each_node.to_a + end + # Yields the link's URL and text whenever the node is a valid <a> tag. def yield_valid_link(node) link = CGI.unescape(node.attr('href').to_s) diff --git a/lib/banzai/filter/reference_gatherer_filter.rb b/lib/banzai/filter/reference_gatherer_filter.rb deleted file mode 100644 index 96fdb06304e..00000000000 --- a/lib/banzai/filter/reference_gatherer_filter.rb +++ /dev/null @@ -1,65 +0,0 @@ -module Banzai - module Filter - # HTML filter that gathers all referenced records that the current user has - # permission to view. - # - # Expected to be run in its own post-processing pipeline. - # - class ReferenceGathererFilter < HTML::Pipeline::Filter - def initialize(*) - super - - result[:references] ||= Hash.new { |hash, type| hash[type] = [] } - end - - def call - Querying.css(doc, 'a.gfm').each do |node| - gather_references(node) - end - - load_lazy_references unless ReferenceExtractor.lazy? - - doc - end - - private - - def gather_references(node) - return unless node.has_attribute?('data-reference-filter') - - reference_type = node.attr('data-reference-filter') - reference_filter = Banzai::Filter.const_get(reference_type) - - return if context[:reference_filter] && reference_filter != context[:reference_filter] - - return if author && !reference_filter.user_can_reference?(author, node, context) - - return unless reference_filter.user_can_see_reference?(current_user, node, context) - - references = reference_filter.referenced_by(node) - return unless references - - references.each do |type, values| - Array.wrap(values).each do |value| - result[:references][type] << value - end - end - end - - def load_lazy_references - refs = result[:references] - refs.each do |type, values| - refs[type] = ReferenceExtractor.lazily(values) - end - end - - def current_user - context[:current_user] - end - - def author - context[:author] - end - end - end -end diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index 42dbab9d27e..ca80aac5a08 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -63,7 +63,7 @@ module Banzai begin uri = Addressable::URI.parse(node['href']) - uri.scheme.strip! if uri.scheme + uri.scheme = uri.scheme.strip.downcase if uri.scheme node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme) rescue Addressable::URI::InvalidURIError diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb index d507eb5ebe1..212a0bbf2a0 100644 --- a/lib/banzai/filter/snippet_reference_filter.rb +++ b/lib/banzai/filter/snippet_reference_filter.rb @@ -5,6 +5,8 @@ module Banzai # # This filter supports cross-project references. class SnippetReferenceFilter < AbstractReferenceFilter + self.reference_type = :snippet + def self.object_class Snippet end diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb index 7edfe5ade2d..c0f503c9af3 100644 --- a/lib/banzai/filter/upload_link_filter.rb +++ b/lib/banzai/filter/upload_link_filter.rb @@ -8,6 +8,8 @@ module Banzai # class UploadLinkFilter < HTML::Pipeline::Filter def call + return doc unless project + doc.search('a').each do |el| process_link_attr el.attribute('href') end @@ -31,7 +33,11 @@ module Banzai end def build_url(uri) - File.join(Gitlab.config.gitlab.url, context[:project].path_with_namespace, uri) + File.join(Gitlab.config.gitlab.url, project.path_with_namespace, uri) + end + + def project + context[:project] end # Ensure that a :project key exists in context diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index eea3af842b6..5b0a6d8541b 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -4,6 +4,8 @@ module Banzai # # A special `@all` reference is also supported. class UserReferenceFilter < ReferenceFilter + self.reference_type = :user + # Public: Find `@user` user references in text # # UserReferenceFilter.references_in(text) do |match, username| @@ -21,50 +23,13 @@ module Banzai end end - def self.referenced_by(node) - if node.has_attribute?('data-group') - group = Group.find(node.attr('data-group')) rescue nil - return unless group - - { user: group.users } - elsif node.has_attribute?('data-user') - { user: LazyReference.new(User, node.attr('data-user')) } - elsif node.has_attribute?('data-project') - project = Project.find(node.attr('data-project')) rescue nil - return unless project - - { user: project.team.members.flatten } - end - end - - def self.user_can_see_reference?(user, node, context) - if node.has_attribute?('data-group') - group = Group.find(node.attr('data-group')) rescue nil - Ability.abilities.allowed?(user, :read_group, group) - else - super - end - end - - def self.user_can_reference?(user, node, context) - # Only team members can reference `@all` - if node.has_attribute?('data-project') - project = Project.find(node.attr('data-project')) rescue nil - return false unless project - - user && project.team.member?(user) - else - super - end - end - def call return doc if project.nil? ref_pattern = User.reference_pattern ref_pattern_start = /\A#{ref_pattern}\z/ - each_node do |node| + nodes.each do |node| if text_node?(node) replace_text_when_pattern_matches(node, ref_pattern) do |content| user_link_filter(content) @@ -94,7 +59,7 @@ module Banzai self.class.references_in(text) do |match, username| if username == 'all' link_to_all(link_text: link_text) - elsif namespace = Namespace.find_by(path: username) + elsif namespace = namespaces[username] link_to_namespace(namespace, link_text: link_text) || match else match @@ -102,6 +67,31 @@ module Banzai end end + # Returns a Hash containing all Namespace objects for the username + # references in the current document. + # + # The keys of this Hash are the namespace paths, the values the + # corresponding Namespace objects. + def namespaces + @namespaces ||= + Namespace.where(path: usernames).each_with_object({}) do |row, hash| + hash[row.path] = row + end + end + + # Returns all usernames referenced in the current document. + def usernames + refs = Set.new + + nodes.each do |node| + node.to_html.scan(User.reference_pattern) do + refs << $~[:user] + end + end + + refs.to_a + end + private def urls @@ -114,9 +104,12 @@ module Banzai def link_to_all(link_text: nil) project = context[:project] + author = context[:author] + url = urls.namespace_project_url(project.namespace, project, only_path: context[:only_path]) - data = data_attribute(project: project.id) + + data = data_attribute(project: project.id, author: author.try(:id)) text = link_text || User.reference_prefix + 'all' link_tag(url, data, text) diff --git a/lib/banzai/filter/wiki_link_filter.rb b/lib/banzai/filter/wiki_link_filter.rb index 06d10c98501..37a2779d453 100644 --- a/lib/banzai/filter/wiki_link_filter.rb +++ b/lib/banzai/filter/wiki_link_filter.rb @@ -2,7 +2,8 @@ require 'uri' module Banzai module Filter - # HTML filter that "fixes" relative links to files in a repository. + # HTML filter that "fixes" links to pages/files in a wiki. + # Rewrite rules are documented in the `WikiPipeline` spec. # # Context options: # :project_wiki @@ -25,31 +26,15 @@ module Banzai end def process_link_attr(html_attr) - return if html_attr.blank? || file_reference?(html_attr) + return if html_attr.blank? - uri = URI(html_attr.value) - if uri.relative? && uri.path.present? - html_attr.value = rebuild_wiki_uri(uri).to_s - end + html_attr.value = apply_rewrite_rules(html_attr.value) rescue URI::Error # noop end - def rebuild_wiki_uri(uri) - uri.path = ::File.join(project_wiki_base_path, uri.path) - uri - end - - def file_reference?(html_attr) - !File.extname(html_attr.value).blank? - end - - def project_wiki - context[:project_wiki] - end - - def project_wiki_base_path - project_wiki && project_wiki.wiki_base_path + def apply_rewrite_rules(link_string) + Rewriter.new(link_string, wiki: context[:project_wiki], slug: context[:page_slug]).apply_rules end end end diff --git a/lib/banzai/filter/wiki_link_filter/rewriter.rb b/lib/banzai/filter/wiki_link_filter/rewriter.rb new file mode 100644 index 00000000000..2e2c8da311e --- /dev/null +++ b/lib/banzai/filter/wiki_link_filter/rewriter.rb @@ -0,0 +1,40 @@ +module Banzai + module Filter + class WikiLinkFilter < HTML::Pipeline::Filter + class Rewriter + def initialize(link_string, wiki:, slug:) + @uri = Addressable::URI.parse(link_string) + @wiki_base_path = wiki && wiki.wiki_base_path + @slug = slug + end + + def apply_rules + apply_file_link_rules! + apply_hierarchical_link_rules! + apply_relative_link_rules! + @uri.to_s + end + + private + + # Of the form 'file.md' + def apply_file_link_rules! + @uri = Addressable::URI.join(@slug, @uri) if @uri.extname.present? + end + + # Of the form `./link`, `../link`, or similar + def apply_hierarchical_link_rules! + @uri = Addressable::URI.join(@slug, @uri) if @uri.to_s[0] == '.' + end + + # Any link _not_ of the form `http://example.com/` + def apply_relative_link_rules! + if @uri.relative? && @uri.path.present? + link = ::File.join(@wiki_base_path, @uri.path) + @uri = Addressable::URI.parse(link) + end + end + end + end + end +end diff --git a/lib/banzai/lazy_reference.rb b/lib/banzai/lazy_reference.rb deleted file mode 100644 index 1095b4debc7..00000000000 --- a/lib/banzai/lazy_reference.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Banzai - class LazyReference - def self.load(refs) - lazy_references, values = refs.partition { |ref| ref.is_a?(self) } - - lazy_values = lazy_references.group_by(&:klass).flat_map do |klass, refs| - ids = refs.flat_map(&:ids) - klass.where(id: ids) - end - - values + lazy_values - end - - attr_reader :klass, :ids - - def initialize(klass, ids) - @klass = klass - @ids = Array.wrap(ids).map(&:to_i) - end - - def load - self.klass.where(id: self.ids) - end - end -end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index ed3cfd6b023..b27ecf3c923 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -23,7 +23,8 @@ module Banzai Filter::LabelReferenceFilter, Filter::MilestoneReferenceFilter, - Filter::TaskListFilter + Filter::TaskListFilter, + Filter::InlineDiffFilter ] end diff --git a/lib/banzai/pipeline/reference_extraction_pipeline.rb b/lib/banzai/pipeline/reference_extraction_pipeline.rb deleted file mode 100644 index 919998380e4..00000000000 --- a/lib/banzai/pipeline/reference_extraction_pipeline.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Banzai - module Pipeline - class ReferenceExtractionPipeline < BasePipeline - def self.filters - FilterArray[ - Filter::ReferenceGathererFilter - ] - end - end - end -end diff --git a/lib/banzai/reference_extractor.rb b/lib/banzai/reference_extractor.rb index f4079538ec5..bf366962aef 100644 --- a/lib/banzai/reference_extractor.rb +++ b/lib/banzai/reference_extractor.rb @@ -1,28 +1,6 @@ module Banzai # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor - class << self - LAZY_KEY = :banzai_reference_extractor_lazy - - def lazy? - Thread.current[LAZY_KEY] - end - - def lazily(values = nil, &block) - return (values || block.call).uniq if lazy? - - begin - Thread.current[LAZY_KEY] = true - - values ||= block.call - - Banzai::LazyReference.load(values.uniq).uniq - ensure - Thread.current[LAZY_KEY] = false - end - end - end - def initialize @texts = [] end @@ -31,23 +9,21 @@ module Banzai @texts << Renderer.render(text, context) end - def references(type, context = {}) - filter = Banzai::Filter["#{type}_reference"] + def references(type, project, current_user = nil) + processor = Banzai::ReferenceParser[type]. + new(project, current_user) + + processor.process(html_documents) + end - context.merge!( - pipeline: :reference_extraction, + private - # ReferenceGathererFilter - reference_filter: filter - ) + def html_documents + # This ensures that we don't memoize anything until we have a number of + # text blobs to parse. + return [] if @texts.empty? - self.class.lazily do - @texts.flat_map do |html| - text_context = context.dup - result = Renderer.render_result(html, text_context) - result[:references][type] - end.uniq - end + @html_documents ||= @texts.map { |html| Nokogiri::HTML.fragment(html) } end end end diff --git a/lib/banzai/reference_parser.rb b/lib/banzai/reference_parser.rb new file mode 100644 index 00000000000..557bec4316e --- /dev/null +++ b/lib/banzai/reference_parser.rb @@ -0,0 +1,14 @@ +module Banzai + module ReferenceParser + # Returns the reference parser class for the given type + # + # Example: + # + # Banzai::ReferenceParser['issue'] + # + # This would return the `Banzai::ReferenceParser::IssueParser` class. + def self.[](name) + const_get("#{name.to_s.camelize}Parser") + end + end +end diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb new file mode 100644 index 00000000000..3d7b9c4a024 --- /dev/null +++ b/lib/banzai/reference_parser/base_parser.rb @@ -0,0 +1,204 @@ +module Banzai + module ReferenceParser + # Base class for reference parsing classes. + # + # Each parser should also specify its reference type by calling + # `self.reference_type = ...` in the body of the class. The value of this + # method should be a symbol such as `:issue` or `:merge_request`. For + # example: + # + # class IssueParser < BaseParser + # self.reference_type = :issue + # end + # + # The reference type is used to determine what nodes to pass to the + # `referenced_by` method. + # + # Parser classes should either implement the instance method + # `references_relation` or overwrite `referenced_by`. The + # `references_relation` method is supposed to return an + # ActiveRecord::Relation used as a base relation for retrieving the objects + # referenced in a set of HTML nodes. + # + # Each class can implement two additional methods: + # + # * `nodes_user_can_reference`: returns an Array of nodes the given user can + # refer to. + # * `nodes_visible_to_user`: returns an Array of nodes that are visible to + # the given user. + # + # You only need to overwrite these methods if you want to tweak who can see + # which references. For example, the IssueParser class defines its own + # `nodes_visible_to_user` method so it can ensure users can only see issues + # they have access to. + class BaseParser + class << self + attr_accessor :reference_type + end + + # Returns the attribute name containing the value for every object to be + # parsed by the current parser. + # + # For example, for a parser class that returns "Animal" objects this + # attribute would be "data-animal". + def self.data_attribute + @data_attribute ||= "data-#{reference_type.to_s.dasherize}" + end + + def initialize(project = nil, current_user = nil) + @project = project + @current_user = current_user + end + + # Returns all the nodes containing references that the user can refer to. + def nodes_user_can_reference(user, nodes) + nodes + end + + # Returns all the nodes that are visible to the given user. + def nodes_visible_to_user(user, nodes) + projects = lazy { projects_for_nodes(nodes) } + project_attr = 'data-project' + + nodes.select do |node| + if node.has_attribute?(project_attr) + node_id = node.attr(project_attr).to_i + + if project && project.id == node_id + true + else + can?(user, :read_project, projects[node_id]) + end + else + true + end + end + end + + # Returns an Array of objects referenced by any of the given HTML nodes. + def referenced_by(nodes) + ids = unique_attribute_values(nodes, self.class.data_attribute) + + references_relation.where(id: ids) + end + + # Returns the ActiveRecord::Relation to use for querying references in the + # DB. + def references_relation + raise NotImplementedError, + "#{self.class} does not implement #{__method__}" + end + + # Returns a Hash containing attribute values per project ID. + # + # The returned Hash uses the following format: + # + # { project id => [value1, value2, ...] } + # + # nodes - An Array of HTML nodes to process. + # attribute - The name of the attribute (as a String) for which to gather + # values. + # + # Returns a Hash. + def gather_attributes_per_project(nodes, attribute) + per_project = Hash.new { |hash, key| hash[key] = Set.new } + + nodes.each do |node| + project_id = node.attr('data-project').to_i + id = node.attr(attribute) + + per_project[project_id] << id if id + end + + per_project + end + + # Returns a Hash containing objects for an attribute grouped per their + # IDs. + # + # The returned Hash uses the following format: + # + # { id value => row } + # + # nodes - An Array of HTML nodes to process. + # + # collection - The model or ActiveRecord relation to use for retrieving + # rows from the database. + # + # attribute - The name of the attribute containing the primary key values + # for every row. + # + # Returns a Hash. + def grouped_objects_for_nodes(nodes, collection, attribute) + return {} if nodes.empty? + + ids = unique_attribute_values(nodes, attribute) + + collection.where(id: ids).each_with_object({}) do |row, hash| + hash[row.id] = row + end + end + + # Returns an Array containing all unique values of an attribute of the + # given nodes. + def unique_attribute_values(nodes, attribute) + values = Set.new + + nodes.each do |node| + if node.has_attribute?(attribute) + values << node.attr(attribute) + end + end + + values.to_a + end + + # Processes the list of HTML documents and returns an Array containing all + # the references. + def process(documents) + type = self.class.reference_type + + nodes = documents.flat_map do |document| + Querying.css(document, "a[data-reference-type='#{type}'].gfm").to_a + end + + gather_references(nodes) + end + + # Gathers the references for the given HTML nodes. + def gather_references(nodes) + nodes = nodes_user_can_reference(current_user, nodes) + nodes = nodes_visible_to_user(current_user, nodes) + + referenced_by(nodes) + end + + # Returns a Hash containing the projects for a given list of HTML nodes. + # + # The returned Hash uses the following format: + # + # { project ID => project } + # + def projects_for_nodes(nodes) + @projects_for_nodes ||= + grouped_objects_for_nodes(nodes, Project, 'data-project') + end + + def can?(user, permission, subject) + Ability.abilities.allowed?(user, permission, subject) + end + + def find_projects_for_hash_keys(hash) + Project.where(id: hash.keys) + end + + private + + attr_reader :current_user, :project + + def lazy(&block) + Gitlab::Lazy.new(&block) + end + end + end +end diff --git a/lib/banzai/reference_parser/commit_parser.rb b/lib/banzai/reference_parser/commit_parser.rb new file mode 100644 index 00000000000..0fee9d267de --- /dev/null +++ b/lib/banzai/reference_parser/commit_parser.rb @@ -0,0 +1,34 @@ +module Banzai + module ReferenceParser + class CommitParser < BaseParser + self.reference_type = :commit + + def referenced_by(nodes) + commit_ids = commit_ids_per_project(nodes) + projects = find_projects_for_hash_keys(commit_ids) + + projects.flat_map do |project| + find_commits(project, commit_ids[project.id]) + end + end + + def commit_ids_per_project(nodes) + gather_attributes_per_project(nodes, self.class.data_attribute) + end + + def find_commits(project, ids) + commits = [] + + return commits unless project.valid_repo? + + ids.each do |id| + commit = project.commit(id) + + commits << commit if commit + end + + commits + end + end + end +end diff --git a/lib/banzai/reference_parser/commit_range_parser.rb b/lib/banzai/reference_parser/commit_range_parser.rb new file mode 100644 index 00000000000..69d01f8db15 --- /dev/null +++ b/lib/banzai/reference_parser/commit_range_parser.rb @@ -0,0 +1,38 @@ +module Banzai + module ReferenceParser + class CommitRangeParser < BaseParser + self.reference_type = :commit_range + + def referenced_by(nodes) + range_ids = commit_range_ids_per_project(nodes) + projects = find_projects_for_hash_keys(range_ids) + + projects.flat_map do |project| + find_ranges(project, range_ids[project.id]) + end + end + + def commit_range_ids_per_project(nodes) + gather_attributes_per_project(nodes, self.class.data_attribute) + end + + def find_ranges(project, range_ids) + ranges = [] + + range_ids.each do |id| + range = find_object(project, id) + + ranges << range if range + end + + ranges + end + + def find_object(project, id) + range = CommitRange.new(id, project) + + range.valid_commits? ? range : nil + end + end + end +end diff --git a/lib/banzai/reference_parser/external_issue_parser.rb b/lib/banzai/reference_parser/external_issue_parser.rb new file mode 100644 index 00000000000..a1264db2111 --- /dev/null +++ b/lib/banzai/reference_parser/external_issue_parser.rb @@ -0,0 +1,25 @@ +module Banzai + module ReferenceParser + class ExternalIssueParser < BaseParser + self.reference_type = :external_issue + + def referenced_by(nodes) + issue_ids = issue_ids_per_project(nodes) + projects = find_projects_for_hash_keys(issue_ids) + issues = [] + + projects.each do |project| + issue_ids[project.id].each do |id| + issues << ExternalIssue.new(id, project) + end + end + + issues + end + + def issue_ids_per_project(nodes) + gather_attributes_per_project(nodes, self.class.data_attribute) + end + end + end +end diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb new file mode 100644 index 00000000000..24076e3d9ec --- /dev/null +++ b/lib/banzai/reference_parser/issue_parser.rb @@ -0,0 +1,40 @@ +module Banzai + module ReferenceParser + class IssueParser < BaseParser + self.reference_type = :issue + + def nodes_visible_to_user(user, nodes) + # It is not possible to check access rights for external issue trackers + return nodes if project && project.external_issue_tracker + + issues = issues_for_nodes(nodes) + + nodes.select do |node| + issue = issue_for_node(issues, node) + + issue ? can?(user, :read_issue, issue) : false + end + end + + def referenced_by(nodes) + issues = issues_for_nodes(nodes) + + nodes.map { |node| issue_for_node(issues, node) }.uniq + end + + def issues_for_nodes(nodes) + @issues_for_nodes ||= grouped_objects_for_nodes( + nodes, + Issue.all.includes(:author, :assignee, :project), + self.class.data_attribute + ) + end + + private + + def issue_for_node(issues, node) + issues[node.attr(self.class.data_attribute).to_i] + end + end + end +end diff --git a/lib/banzai/reference_parser/label_parser.rb b/lib/banzai/reference_parser/label_parser.rb new file mode 100644 index 00000000000..e5d1eb11d7f --- /dev/null +++ b/lib/banzai/reference_parser/label_parser.rb @@ -0,0 +1,11 @@ +module Banzai + module ReferenceParser + class LabelParser < BaseParser + self.reference_type = :label + + def references_relation + Label + end + end + end +end diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb new file mode 100644 index 00000000000..c9a9ca79c09 --- /dev/null +++ b/lib/banzai/reference_parser/merge_request_parser.rb @@ -0,0 +1,11 @@ +module Banzai + module ReferenceParser + class MergeRequestParser < BaseParser + self.reference_type = :merge_request + + def references_relation + MergeRequest.includes(:author, :assignee, :target_project) + end + end + end +end diff --git a/lib/banzai/reference_parser/milestone_parser.rb b/lib/banzai/reference_parser/milestone_parser.rb new file mode 100644 index 00000000000..a000ac61e5c --- /dev/null +++ b/lib/banzai/reference_parser/milestone_parser.rb @@ -0,0 +1,11 @@ +module Banzai + module ReferenceParser + class MilestoneParser < BaseParser + self.reference_type = :milestone + + def references_relation + Milestone + end + end + end +end diff --git a/lib/banzai/reference_parser/snippet_parser.rb b/lib/banzai/reference_parser/snippet_parser.rb new file mode 100644 index 00000000000..fa71b3c952a --- /dev/null +++ b/lib/banzai/reference_parser/snippet_parser.rb @@ -0,0 +1,11 @@ +module Banzai + module ReferenceParser + class SnippetParser < BaseParser + self.reference_type = :snippet + + def references_relation + Snippet + end + end + end +end diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb new file mode 100644 index 00000000000..a12b0d19560 --- /dev/null +++ b/lib/banzai/reference_parser/user_parser.rb @@ -0,0 +1,92 @@ +module Banzai + module ReferenceParser + class UserParser < BaseParser + self.reference_type = :user + + def referenced_by(nodes) + group_ids = [] + user_ids = [] + project_ids = [] + + nodes.each do |node| + if node.has_attribute?('data-group') + group_ids << node.attr('data-group').to_i + elsif node.has_attribute?(self.class.data_attribute) + user_ids << node.attr(self.class.data_attribute).to_i + elsif node.has_attribute?('data-project') + project_ids << node.attr('data-project').to_i + end + end + + find_users_for_groups(group_ids) | find_users(user_ids) | + find_users_for_projects(project_ids) + end + + def nodes_visible_to_user(user, nodes) + group_attr = 'data-group' + groups = lazy { grouped_objects_for_nodes(nodes, Group, group_attr) } + visible = [] + remaining = [] + + nodes.each do |node| + if node.has_attribute?(group_attr) + node_group = groups[node.attr(group_attr).to_i] + + if node_group && + can?(user, :read_group, node_group) + visible << node + end + # Remaining nodes will be processed by the parent class' + # implementation of this method. + else + remaining << node + end + end + + visible + super(current_user, remaining) + end + + def nodes_user_can_reference(current_user, nodes) + project_attr = 'data-project' + author_attr = 'data-author' + + projects = lazy { projects_for_nodes(nodes) } + users = lazy { grouped_objects_for_nodes(nodes, User, author_attr) } + + nodes.select do |node| + project_id = node.attr(project_attr) + user_id = node.attr(author_attr) + + if project && project_id && project.id == project_id.to_i + true + elsif project_id && user_id + project = projects[project_id.to_i] + user = users[user_id.to_i] + + project && user ? project.team.member?(user) : false + else + true + end + end + end + + def find_users(ids) + return [] if ids.empty? + + User.where(id: ids).to_a + end + + def find_users_for_groups(ids) + return [] if ids.empty? + + User.joins(:group_members).where(members: { source_id: ids }).to_a + end + + def find_users_for_projects(ids) + return [] if ids.empty? + + Project.where(id: ids).flat_map { |p| p.team.members.to_a } + end + end + end +end diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb index ac6d667cf8d..229050151d3 100644 --- a/lib/ci/ansi2html.rb +++ b/lib/ci/ansi2html.rb @@ -23,8 +23,8 @@ module Ci cross: 0x10, } - def self.convert(ansi) - Converter.new().convert(ansi) + def self.convert(ansi, state = nil) + Converter.new.convert(ansi, state) end class Converter @@ -84,22 +84,38 @@ module Ci def on_107(s) set_bg_color(7, 'l') end def on_109(s) set_bg_color(9, 'l') end - def convert(ansi) - @out = "" - @n_open_tags = 0 - reset() + attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask + + STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask] + + def convert(raw, new_state) + reset_state + restore_state(raw, new_state) if new_state.present? + + start = @offset + ansi = raw[@offset..-1] + + open_new_tag - s = StringScanner.new(ansi.gsub("<", "<")) - while(!s.eos?) + s = StringScanner.new(ansi) + until s.eos? if s.scan(/\e([@-_])(.*?)([@-~])/) handle_sequence(s) + elsif s.scan(/\e(([@-_])(.*?)?)?$/) + break + elsif s.scan(/</) + @out << '<' + elsif s.scan(/\n/) + @out << '<br>' else @out << s.scan(/./m) end + @offset += s.matched_size end close_open_tags() - @out + + { state: state, html: @out, text: ansi[0, @offset - start], append: start > 0 } end def handle_sequence(s) @@ -121,6 +137,20 @@ module Ci evaluate_command_stack(commands) + open_new_tag + end + + def evaluate_command_stack(stack) + return unless command = stack.shift() + + if self.respond_to?("on_#{command}", true) + self.send("on_#{command}", stack) + end + + evaluate_command_stack(stack) + end + + def open_new_tag css_classes = [] unless @fg_color.nil? @@ -138,20 +168,8 @@ module Ci css_classes << "term-#{css_class}" if @style_mask & flag != 0 end - open_new_tag(css_classes) if css_classes.length > 0 - end + return if css_classes.empty? - def evaluate_command_stack(stack) - return unless command = stack.shift() - - if self.respond_to?("on_#{command}", true) - self.send("on_#{command}", stack) - end - - evaluate_command_stack(stack) - end - - def open_new_tag(css_classes) @out << %{<span class="#{css_classes.join(' ')}">} @n_open_tags += 1 end @@ -163,6 +181,31 @@ module Ci end end + def reset_state + @offset = 0 + @n_open_tags = 0 + @out = '' + reset + end + + def state + state = STATE_PARAMS.inject({}) do |h, param| + h[param] = send(param) + h + end + Base64.urlsafe_encode64(state.to_json) + end + + def restore_state(raw, new_state) + state = Base64.urlsafe_decode64(new_state) + state = JSON.parse(state, symbolize_names: true) + return if state[:offset].to_i > raw.length + + STATE_PARAMS.each do |param| + send("#{param}=".to_sym, state[param]) + end + end + def reset @fg_color = nil @bg_color = nil diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb index 353c4ddebf8..17bb99a2ae5 100644 --- a/lib/ci/api/api.rb +++ b/lib/ci/api/api.rb @@ -1,9 +1,7 @@ -Dir["#{Rails.root}/lib/ci/api/*.rb"].each {|file| require file} - module Ci module API class API < Grape::API - include APIGuard + include ::API::APIGuard version 'v1', using: :path rescue_from ActiveRecord::RecordNotFound do @@ -31,9 +29,9 @@ module Ci helpers ::API::Helpers helpers Gitlab::CurrentSettings - mount Builds - mount Runners - mount Triggers + mount ::Ci::API::Builds + mount ::Ci::API::Runners + mount ::Ci::API::Triggers end end end diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb index b25e0e573a8..a902ced35d7 100644 --- a/lib/ci/api/entities.rb +++ b/lib/ci/api/entities.rb @@ -56,7 +56,7 @@ module Ci class TriggerRequest < Grape::Entity expose :id, :variables - expose :commit, using: Commit + expose :pipeline, using: Commit, as: :commit end end end diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb index 192b1d18a51..0c41f22c7c5 100644 --- a/lib/ci/api/runners.rb +++ b/lib/ci/api/runners.rb @@ -28,20 +28,20 @@ module Ci post "register" do required_attributes! [:token] + attributes = { description: params[:description], + tag_list: params[:tag_list] } + + unless params[:run_untagged].nil? + attributes[:run_untagged] = params[:run_untagged] + end + runner = if runner_registration_token_valid? # Create shared runner. Requires admin access - Ci::Runner.create( - description: params[:description], - tag_list: params[:tag_list], - is_shared: true - ) + Ci::Runner.create(attributes.merge(is_shared: true)) elsif project = Project.find_by(runners_token: params[:token]) # Create a specific runner for project. - project.runners.create( - description: params[:description], - tag_list: params[:tag_list] - ) + project.runners.create(attributes) end return forbidden! unless runner diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb index d53bdcbd0f2..5270108ef0f 100644 --- a/lib/ci/charts.rb +++ b/lib/ci/charts.rb @@ -60,11 +60,12 @@ module Ci class BuildTime < Chart def collect - commits = project.ci_commits.last(30) + commits = project.pipelines.last(30) commits.each do |commit| @labels << commit.short_sha - @build_times << (commit.duration / 60) + duration = commit.duration || 0 + @build_times << (duration / 60) end end end diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 504d3df9d34..130f5b0892e 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -1,6 +1,6 @@ module Ci class GitlabCiYamlProcessor - class ValidationError < StandardError;end + class ValidationError < StandardError; end DEFAULT_STAGES = %w(build test deploy) DEFAULT_STAGE = 'test' @@ -12,18 +12,14 @@ module Ci attr_reader :before_script, :after_script, :image, :services, :path, :cache def initialize(config, path = nil) - @config = YAML.safe_load(config, [Symbol], [], true) + @config = Gitlab::Ci::Config.new(config).to_hash @path = path - unless @config.is_a? Hash - raise ValidationError, "YAML should be a hash" - end - - @config = @config.deep_symbolize_keys - initial_parsing validate! + rescue Gitlab::Ci::Config::Loader::FormatError => e + raise ValidationError, e.message end def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil) @@ -265,7 +261,7 @@ module Ci end def validate_job_dependencies!(name, job) - if !validate_array_of_strings(job[:dependencies]) + unless validate_array_of_strings(job[:dependencies]) raise ValidationError, "#{name} job: dependencies parameter should be an array of strings" end diff --git a/lib/container_registry/blob.rb b/lib/container_registry/blob.rb new file mode 100644 index 00000000000..4e20dc4f875 --- /dev/null +++ b/lib/container_registry/blob.rb @@ -0,0 +1,48 @@ +module ContainerRegistry + class Blob + attr_reader :repository, :config + + delegate :registry, :client, to: :repository + + def initialize(repository, config) + @repository = repository + @config = config || {} + end + + def valid? + digest.present? + end + + def path + "#{repository.path}@#{digest}" + end + + def digest + config['digest'] + end + + def type + config['mediaType'] + end + + def size + config['size'] + end + + def revision + digest.split(':')[1] + end + + def short_revision + revision[0..8] + end + + def delete + client.delete_blob(repository.name, digest) + end + + def data + @data ||= client.blob(repository.name, digest, type) + end + end +end diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb new file mode 100644 index 00000000000..4d726692f45 --- /dev/null +++ b/lib/container_registry/client.rb @@ -0,0 +1,61 @@ +require 'faraday' +require 'faraday_middleware' + +module ContainerRegistry + class Client + attr_accessor :uri + + MANIFEST_VERSION = 'application/vnd.docker.distribution.manifest.v2+json' + + def initialize(base_uri, options = {}) + @base_uri = base_uri + @faraday = Faraday.new(@base_uri) do |conn| + initialize_connection(conn, options) + end + end + + def repository_tags(name) + @faraday.get("/v2/#{name}/tags/list").body + end + + def repository_manifest(name, reference) + @faraday.get("/v2/#{name}/manifests/#{reference}").body + end + + def repository_tag_digest(name, reference) + response = @faraday.head("/v2/#{name}/manifests/#{reference}") + response.headers['docker-content-digest'] if response.success? + end + + def delete_repository_tag(name, reference) + @faraday.delete("/v2/#{name}/manifests/#{reference}").success? + end + + def blob(name, digest, type = nil) + headers = {} + headers['Accept'] = type if type + @faraday.get("/v2/#{name}/blobs/#{digest}", nil, headers).body + end + + def delete_blob(name, digest) + @faraday.delete("/v2/#{name}/blobs/#{digest}").success? + end + + private + + def initialize_connection(conn, options) + conn.request :json + conn.headers['Accept'] = MANIFEST_VERSION + + conn.response :json, content_type: /\bjson$/ + + if options[:user] && options[:password] + conn.request(:basic_auth, options[:user].to_s, options[:password].to_s) + elsif options[:token] + conn.request(:authorization, :bearer, options[:token].to_s) + end + + conn.adapter :net_http + end + end +end diff --git a/lib/container_registry/config.rb b/lib/container_registry/config.rb new file mode 100644 index 00000000000..589f9f4380a --- /dev/null +++ b/lib/container_registry/config.rb @@ -0,0 +1,16 @@ +module ContainerRegistry + class Config + attr_reader :tag, :blob, :data + + def initialize(tag, blob) + @tag, @blob = tag, blob + @data = JSON.parse(blob.data) + end + + def [](key) + return unless data + + data[key] + end + end +end diff --git a/lib/container_registry/registry.rb b/lib/container_registry/registry.rb new file mode 100644 index 00000000000..0e634f6b6ef --- /dev/null +++ b/lib/container_registry/registry.rb @@ -0,0 +1,21 @@ +module ContainerRegistry + class Registry + attr_reader :uri, :client, :path + + def initialize(uri, options = {}) + @uri = uri + @path = options[:path] || default_path + @client = ContainerRegistry::Client.new(uri, options) + end + + def repository(name) + ContainerRegistry::Repository.new(self, name) + end + + private + + def default_path + @uri.sub(/^https?:\/\//, '') + end + end +end diff --git a/lib/container_registry/repository.rb b/lib/container_registry/repository.rb new file mode 100644 index 00000000000..0e4a7cb3cc9 --- /dev/null +++ b/lib/container_registry/repository.rb @@ -0,0 +1,48 @@ +module ContainerRegistry + class Repository + attr_reader :registry, :name + + delegate :client, to: :registry + + def initialize(registry, name) + @registry, @name = registry, name + end + + def path + [registry.path, name].compact.join('/') + end + + def tag(tag) + ContainerRegistry::Tag.new(self, tag) + end + + def manifest + return @manifest if defined?(@manifest) + + @manifest = client.repository_tags(name) + end + + def valid? + manifest.present? + end + + def tags + return @tags if defined?(@tags) + return [] unless manifest && manifest['tags'] + + @tags = manifest['tags'].map do |tag| + ContainerRegistry::Tag.new(self, tag) + end + end + + def blob(config) + ContainerRegistry::Blob.new(self, config) + end + + def delete_tags + return unless tags + + tags.all?(&:delete) + end + end +end diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb new file mode 100644 index 00000000000..43f8d6dc8c2 --- /dev/null +++ b/lib/container_registry/tag.rb @@ -0,0 +1,77 @@ +module ContainerRegistry + class Tag + attr_reader :repository, :name + + delegate :registry, :client, to: :repository + + def initialize(repository, name) + @repository, @name = repository, name + end + + def valid? + manifest.present? + end + + def manifest + return @manifest if defined?(@manifest) + + @manifest = client.repository_manifest(repository.name, name) + end + + def path + "#{repository.path}:#{name}" + end + + def [](key) + return unless manifest + + manifest[key] + end + + def digest + return @digest if defined?(@digest) + + @digest = client.repository_tag_digest(repository.name, name) + end + + def config_blob + return @config_blob if defined?(@config_blob) + return unless manifest && manifest['config'] + + @config_blob = repository.blob(manifest['config']) + end + + def config + return unless config_blob + + @config ||= ContainerRegistry::Config.new(self, config_blob) + end + + def created_at + return unless config + + @created_at ||= DateTime.rfc3339(config['created']) + end + + def layers + return @layers if defined?(@layers) + return unless manifest + + @layers = manifest['layers'].map do |layer| + repository.blob(layer) + end + end + + def total_size + return unless layers + + layers.map(&:size).sum + end + + def delete + return unless digest + + client.delete_repository_tag(repository.name, digest) + end + end +end diff --git a/lib/event_filter.rb b/lib/event_filter.rb index f15b2cfd231..668d2fa41b3 100644 --- a/lib/event_filter.rb +++ b/lib/event_filter.rb @@ -27,7 +27,7 @@ class EventFilter @params = if params params.dup else - []#EventFilter.default_filter + [] # EventFilter.default_filter end end diff --git a/lib/gitlab.rb b/lib/gitlab.rb index 7479e729db1..37f4c34054f 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -1,4 +1,4 @@ -require 'gitlab/git' +require_dependency 'gitlab/git' module Gitlab def self.com? diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 30509528b8b..076e2af7d38 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -1,17 +1,86 @@ module Gitlab - class Auth - def find(login, password) - user = User.by_login(login) - - # If no user is found, or it's an LDAP server, try LDAP. - # LDAP users are only authenticated via LDAP - if user.nil? || user.ldap_user? - # Second chance - try LDAP authentication - return nil unless Gitlab::LDAP::Config.enabled? - - Gitlab::LDAP::Authentication.login(login, password) - else - user if user.valid_password?(password) + module Auth + Result = Struct.new(:user, :type) + + class << self + def find(login, password, project:, ip:) + raise "Must provide an IP for rate limiting" if ip.nil? + + result = Result.new + + if valid_ci_request?(login, password, project) + result.type = :ci + elsif result.user = find_in_gitlab_or_ldap(login, password) + result.type = :gitlab_or_ldap + elsif result.user = oauth_access_token_check(login, password) + result.type = :oauth + end + + rate_limit!(ip, success: !!result.user || (result.type == :ci), login: login) + result + end + + def find_in_gitlab_or_ldap(login, password) + user = User.by_login(login) + + # If no user is found, or it's an LDAP server, try LDAP. + # LDAP users are only authenticated via LDAP + if user.nil? || user.ldap_user? + # Second chance - try LDAP authentication + return nil unless Gitlab::LDAP::Config.enabled? + + Gitlab::LDAP::Authentication.login(login, password) + else + user if user.valid_password?(password) + end + end + + def rate_limit!(ip, success:, login:) + rate_limiter = Gitlab::Auth::IpRateLimiter.new(ip) + return unless rate_limiter.enabled? + + if success + # Repeated login 'failures' are normal behavior for some Git clients so + # it is important to reset the ban counter once the client has proven + # they are not a 'bad guy'. + rate_limiter.reset! + else + # Register a login failure so that Rack::Attack can block the next + # request from this IP if needed. + rate_limiter.register_fail! + + if rate_limiter.banned? + Rails.logger.info "IP #{ip} failed to login " \ + "as #{login} but has been temporarily banned from Git auth" + end + end + end + + private + + def valid_ci_request?(login, password, project) + matched_login = /(?<service>^[a-zA-Z]*-ci)-token$/.match(login) + + return false unless project && matched_login.present? + + underscored_service = matched_login['service'].underscore + + if underscored_service == 'gitlab_ci' + project && project.valid_build_token?(password) + elsif Service.available_services_names.include?(underscored_service) + # We treat underscored_service as a trusted input because it is included + # in the Service.available_services_names whitelist. + service = project.public_send("#{underscored_service}_service") + + service && service.activated? && service.valid_token?(password) + end + end + + def oauth_access_token_check(login, password) + if login == "oauth2" && password.present? + token = Doorkeeper::AccessToken.by_token(password) + token && token.accessible? && User.find_by(id: token.resource_owner_id) + end end end end diff --git a/lib/gitlab/auth/ip_rate_limiter.rb b/lib/gitlab/auth/ip_rate_limiter.rb new file mode 100644 index 00000000000..1089bc9f89e --- /dev/null +++ b/lib/gitlab/auth/ip_rate_limiter.rb @@ -0,0 +1,42 @@ +module Gitlab + module Auth + class IpRateLimiter + attr_reader :ip + + def initialize(ip) + @ip = ip + @banned = false + end + + def enabled? + config.enabled + end + + def reset! + Rack::Attack::Allow2Ban.reset(ip, config) + end + + def register_fail! + # Allow2Ban.filter will return false if this IP has not failed too often yet + @banned = Rack::Attack::Allow2Ban.filter(ip, config) do + # If we return false here, the failure for this IP is ignored by Allow2Ban + ip_can_be_banned? + end + end + + def banned? + @banned + end + + private + + def config + Gitlab.config.rack_attack.git_basic_auth + end + + def ip_can_be_banned? + config.ip_whitelist.exclude?(ip) + end + end + end +end diff --git a/lib/gitlab/award_emoji.rb b/lib/gitlab/award_emoji.rb new file mode 100644 index 00000000000..51b1df9ecbd --- /dev/null +++ b/lib/gitlab/award_emoji.rb @@ -0,0 +1,84 @@ +module Gitlab + class AwardEmoji + CATEGORIES = { + other: "Other", + objects: "Objects", + places: "Places", + travel_places: "Travel", + emoticons: "Emoticons", + objects_symbols: "Symbols", + nature: "Nature", + celebration: "Celebration", + people: "People", + activity: "Activity", + flags: "Flags", + food_drink: "Food" + }.with_indifferent_access + + CATEGORY_ALIASES = { + symbols: "objects_symbols", + foods: "food_drink", + travel: "travel_places" + }.with_indifferent_access + + def self.normalize_emoji_name(name) + aliases[name] || name + end + + def self.emoji_by_category + unless @emoji_by_category + @emoji_by_category = Hash.new { |h, key| h[key] = [] } + + emojis.each do |emoji_name, data| + data["name"] = emoji_name + + # Skip Fitzpatrick(tone) modifiers + next if data["category"] == "modifier" + + category = CATEGORY_ALIASES[data["category"]] || data["category"] + + @emoji_by_category[category] << data + end + + @emoji_by_category = @emoji_by_category.sort.to_h + end + + @emoji_by_category + end + + def self.emojis + @emojis ||= + begin + json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' ) + JSON.parse(File.read(json_path)) + end + end + + def self.aliases + @aliases ||= + begin + json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' ) + JSON.parse(File.read(json_path)) + end + end + + # Returns an Array of Emoji names and their asset URLs. + def self.urls + @urls ||= begin + path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') + prefix = Gitlab::Application.config.assets.prefix + digest = Gitlab::Application.config.assets.digest + + JSON.parse(File.read(path)).map do |hash| + if digest + fname = "#{hash['unicode']}-#{hash['digest']}" + else + fname = hash['unicode'] + end + + { name: hash['name'], path: "#{prefix}/#{fname}.png" } + end + end + end + end +end diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index cdcaae8094c..9e09d2e118d 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -36,10 +36,7 @@ module Grack lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call return lfs_response unless lfs_response.nil? - if project && authorized_request? - # Tell gitlab-workhorse the request is OK, and what the GL_ID is - render_grack_auth_ok - elsif @user.nil? && !@ci + if @user.nil? && !@ci unauthorized else render_not_found @@ -98,7 +95,7 @@ module Grack end def authenticate_user(login, password) - user = Gitlab::Auth.new.find(login, password) + user = Gitlab::Auth.find_in_gitlab_or_ldap(login, password) unless user user = oauth_access_token_check(login, password) @@ -141,36 +138,6 @@ module Grack user end - def authorized_request? - return true if @ci - - case git_cmd - when *Gitlab::GitAccess::DOWNLOAD_COMMANDS - if !Gitlab.config.gitlab_shell.upload_pack - false - elsif user - Gitlab::GitAccess.new(user, project).download_access_check.allowed? - elsif project.public? - # Allow clone/fetch for public projects - true - else - false - end - when *Gitlab::GitAccess::PUSH_COMMANDS - if !Gitlab.config.gitlab_shell.receive_pack - false - elsif user - # Skip user authorization on upload request. - # It will be done by the pre-receive hook in the repository. - true - else - false - end - else - false - end - end - def git_cmd if @request.get? @request.params['service'] @@ -197,24 +164,6 @@ module Grack end end - def render_grack_auth_ok - repo_path = - if @request.path_info =~ /^([\w\.\/-]+)\.wiki\.git/ - ProjectWiki.new(project).repository.path_to_repo - else - project.repository.path_to_repo - end - - [ - 200, - { "Content-Type" => "application/json" }, - [JSON.dump({ - 'GL_ID' => Gitlab::ShellEnv.gl_id(@user), - 'RepoPath' => repo_path, - })] - ] - end - def render_not_found [404, { "Content-Type" => "text/plain" }, ["Not Found"]] end diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 132f9cd1966..3e3986d6382 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -180,7 +180,7 @@ module Gitlab # exists?('gitlab/cookies.git') # def exists?(dir_name) - File.exists?(full_path(dir_name)) + File.exist?(full_path(dir_name)) end protected diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb index 9b83292ef33..8d1ad62fae0 100644 --- a/lib/gitlab/bitbucket_import/client.rb +++ b/lib/gitlab/bitbucket_import/client.rb @@ -121,7 +121,7 @@ module Gitlab def get(url) response = api.get(url) - raise Unauthorized if (400..499).include?(response.code.to_i) + raise Unauthorized if (400..499).cover?(response.code.to_i) response end diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb index 941f818b847..b90ef0b0fba 100644 --- a/lib/gitlab/bitbucket_import/project_creator.rb +++ b/lib/gitlab/bitbucket_import/project_creator.rb @@ -11,7 +11,7 @@ module Gitlab end def execute - project = ::Projects::CreateService.new( + ::Projects::CreateService.new( current_user, name: repo["name"], path: repo["slug"], @@ -21,11 +21,8 @@ module Gitlab import_type: "bitbucket", import_source: "#{repo["owner"]}/#{repo["slug"]}", import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git", + import_data: { credentials: { bb_session: session_data } } ).execute - - project.create_or_update_import_data(credentials: { bb_session: session_data }) - - project end end end diff --git a/lib/gitlab/build_data_builder.rb b/lib/gitlab/build_data_builder.rb index 34e949130da..9f45aefda0f 100644 --- a/lib/gitlab/build_data_builder.rb +++ b/lib/gitlab/build_data_builder.rb @@ -3,7 +3,7 @@ module Gitlab class << self def build(build) project = build.project - commit = build.commit + commit = build.pipeline user = build.user data = { diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb index f2020c82d40..cd2e83b4c27 100644 --- a/lib/gitlab/ci/build/artifacts/metadata.rb +++ b/lib/gitlab/ci/build/artifacts/metadata.rb @@ -56,7 +56,7 @@ module Gitlab child_pattern = '[^/]*/?$' unless @opts[:recursive] match_pattern = /^#{Regexp.escape(@path)}#{child_pattern}/ - until gz.eof? do + until gz.eof? begin path = read_string(gz).force_encoding('UTF-8') meta = read_string(gz).force_encoding('UTF-8') diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb new file mode 100644 index 00000000000..ffe633d4b63 --- /dev/null +++ b/lib/gitlab/ci/config.rb @@ -0,0 +1,16 @@ +module Gitlab + module Ci + class Config + class LoaderError < StandardError; end + + def initialize(config) + loader = Loader.new(config) + @config = loader.load! + end + + def to_hash + @config + end + end + end +end diff --git a/lib/gitlab/ci/config/loader.rb b/lib/gitlab/ci/config/loader.rb new file mode 100644 index 00000000000..dbf6eb0edbe --- /dev/null +++ b/lib/gitlab/ci/config/loader.rb @@ -0,0 +1,25 @@ +module Gitlab + module Ci + class Config + class Loader + class FormatError < StandardError; end + + def initialize(config) + @config = YAML.safe_load(config, [Symbol], [], true) + end + + def valid? + @config.is_a?(Hash) + end + + def load! + unless valid? + raise FormatError, 'Invalid configuration format' + end + + @config.deep_symbolize_keys + end + end + end + end +end diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index 85583dce9ee..9dc2602867e 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -19,7 +19,7 @@ module Gitlab select('date(created_at) as date, count(id) as total_amount'). map(&:attributes) - dates = (1.year.ago.to_date..(Date.today + 1.day)).to_a + dates = (1.year.ago.to_date..Date.today).to_a dates.each do |date| date_id = date.to_time.to_i.to_s diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index f44d1b3a44e..5e7532f57ae 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -1,18 +1,22 @@ module Gitlab module CurrentSettings def current_application_settings - key = :current_application_settings - - RequestStore.store[key] ||= begin - settings = nil + if RequestStore.active? + RequestStore.fetch(:current_application_settings) { ensure_application_settings! } + else + ensure_application_settings! + end + end - if connect_to_db? - settings = ::ApplicationSetting.current - settings ||= ::ApplicationSetting.create_from_defaults unless ActiveRecord::Migrator.needs_migration? - end + def ensure_application_settings! + settings = ::ApplicationSetting.cached - settings || fake_application_settings + if !settings && connect_to_db? + settings = ::ApplicationSetting.current + settings ||= ::ApplicationSetting.create_from_defaults unless ActiveRecord::Migrator.needs_migration? end + + settings || fake_application_settings end def fake_application_settings @@ -22,7 +26,10 @@ module Gitlab signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], gravatar_enabled: Settings.gravatar['enabled'], - sign_in_text: Settings.extra['sign_in_text'], + sign_in_text: nil, + after_sign_up_text: nil, + help_page_text: nil, + shared_runners_text: nil, restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], max_attachment_size: Settings.gitlab['max_attachment_size'], session_expire_delay: Settings.gitlab['session_expire_delay'], @@ -36,6 +43,7 @@ module Gitlab two_factor_grace_period: 48, akismet_enabled: false, repository_checks_enabled: true, + container_registry_token_expire_delay: 5, ) end diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 6f9da69983a..04fa6a3a5de 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -5,17 +5,31 @@ module Gitlab end def self.mysql? - adapter_name.downcase == 'mysql2' + adapter_name.casecmp('mysql2').zero? end def self.postgresql? - adapter_name.downcase == 'postgresql' + adapter_name.casecmp('postgresql').zero? end def self.version database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1] end + def self.nulls_last_order(field, direction = 'ASC') + order = "#{field} #{direction}" + + if Gitlab::Database.postgresql? + order << ' NULLS LAST' + else + # `field IS NULL` will be `0` for non-NULL columns and `1` for NULL + # columns. In the (default) ascending order, `0` comes first. + order.prepend("#{field} IS NULL, ") if direction == 'ASC' + end + + order + end + def true_value if Gitlab::Database.postgresql? "'t'" diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb new file mode 100644 index 00000000000..978c3f7896d --- /dev/null +++ b/lib/gitlab/database/migration_helpers.rb @@ -0,0 +1,142 @@ +module Gitlab + module Database + module MigrationHelpers + # Creates a new index, concurrently when supported + # + # On PostgreSQL this method creates an index concurrently, on MySQL this + # creates a regular index. + # + # Example: + # + # add_concurrent_index :users, :some_column + # + # See Rails' `add_index` for more info on the available arguments. + def add_concurrent_index(table_name, column_name, options = {}) + if transaction_open? + raise 'add_concurrent_index can not be run inside a transaction, ' \ + 'you can disable transactions by calling disable_ddl_transaction! ' \ + 'in the body of your migration class' + end + + if Database.postgresql? + options = options.merge({ algorithm: :concurrently }) + end + + add_index(table_name, column_name, options) + end + + # Updates the value of a column in batches. + # + # This method updates the table in batches of 5% of the total row count. + # Any data inserted while running this method (or after it has finished + # running) is _not_ updated automatically. + # + # This method _only_ updates rows where the column's value is set to NULL. + # + # table - The name of the table. + # column - The name of the column to update. + # value - The value for the column. + def update_column_in_batches(table, column, value) + quoted_table = quote_table_name(table) + quoted_column = quote_column_name(column) + + ## + # Workaround for #17711 + # + # It looks like for MySQL `ActiveRecord::Base.conntection.quote(true)` + # returns correct value (1), but `ActiveRecord::Migration.new.quote` + # returns incorrect value ('true'), which causes migrations to fail. + # + quoted_value = connection.quote(value) + processed = 0 + + total = exec_query("SELECT COUNT(*) AS count FROM #{quoted_table}"). + to_hash. + first['count']. + to_i + + # Update in batches of 5% + batch_size = ((total / 100.0) * 5.0).ceil + + while processed < total + start_row = exec_query(%Q{ + SELECT id + FROM #{quoted_table} + ORDER BY id ASC + LIMIT 1 OFFSET #{processed} + }).to_hash.first + + stop_row = exec_query(%Q{ + SELECT id + FROM #{quoted_table} + ORDER BY id ASC + LIMIT 1 OFFSET #{processed + batch_size} + }).to_hash.first + + query = %Q{ + UPDATE #{quoted_table} + SET #{quoted_column} = #{quoted_value} + WHERE id >= #{start_row['id']} + } + + if stop_row + query += " AND id < #{stop_row['id']}" + end + + execute(query) + + processed += batch_size + end + end + + # Adds a column with a default value without locking an entire table. + # + # This method runs the following steps: + # + # 1. Add the column with a default value of NULL. + # 2. Update all existing rows in batches. + # 3. Change the default value of the column to the specified value. + # 4. Update any remaining rows. + # + # These steps ensure a column can be added to a large and commonly used + # table without locking the entire table for the duration of the table + # modification. + # + # table - The name of the table to update. + # column - The name of the column to add. + # type - The column type (e.g. `:integer`). + # default - The default value for the column. + # allow_null - When set to `true` the column will allow NULL values, the + # default is to not allow NULL values. + def add_column_with_default(table, column, type, default:, allow_null: false) + if transaction_open? + raise 'add_column_with_default can not be run inside a transaction, ' \ + 'you can disable transactions by calling disable_ddl_transaction! ' \ + 'in the body of your migration class' + end + + transaction do + add_column(table, column, type, default: nil) + + # Changing the default before the update ensures any newly inserted + # rows already use the proper default value. + change_column_default(table, column, default) + end + + begin + transaction do + update_column_in_batches(table, column, default) + end + # We want to rescue _all_ exceptions here, even those that don't inherit + # from StandardError. + rescue Exception => error # rubocop: disable all + remove_column(table, column) + + raise error + end + + change_column_null(table, column, false) unless allow_null + end + end + end +end diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb index dccb717e95d..87a9b1e23ac 100644 --- a/lib/gitlab/diff/inline_diff_marker.rb +++ b/lib/gitlab/diff/inline_diff_marker.rb @@ -1,6 +1,11 @@ module Gitlab module Diff class InlineDiffMarker + MARKDOWN_SYMBOLS = { + addition: "+", + deletion: "-" + } + attr_accessor :raw_line, :rich_line def initialize(raw_line, rich_line = raw_line) @@ -8,7 +13,7 @@ module Gitlab @rich_line = ERB::Util.html_escape(rich_line) end - def mark(line_inline_diffs) + def mark(line_inline_diffs, mode: nil, markdown: false) return rich_line unless line_inline_diffs marker_ranges = [] @@ -20,13 +25,22 @@ module Gitlab end offset = 0 - # Mark each range - marker_ranges.each_with_index do |range, i| - class_names = ["idiff"] - class_names << "left" if i == 0 - class_names << "right" if i == marker_ranges.length - 1 - offset = insert_around_range(rich_line, range, "<span class='#{class_names.join(" ")}'>", "</span>", offset) + # Mark each range + marker_ranges.each_with_index do |range, index| + before_content = + if markdown + "{#{MARKDOWN_SYMBOLS[mode]}" + else + "<span class='#{html_class_names(marker_ranges, mode, index)}'>" + end + after_content = + if markdown + "#{MARKDOWN_SYMBOLS[mode]}}" + else + "</span>" + end + offset = insert_around_range(rich_line, range, before_content, after_content, offset) end rich_line.html_safe @@ -34,6 +48,14 @@ module Gitlab private + def html_class_names(marker_ranges, mode, index) + class_names = ["idiff"] + class_names << "left" if index == 0 + class_names << "right" if index == marker_ranges.length - 1 + class_names << mode if mode + class_names.join(" ") + end + # Mapping of character positions in the raw line, to the rich (highlighted) line def position_mapping @position_mapping ||= begin diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index d0815fc7eea..522dd2b9428 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -17,16 +17,16 @@ module Gitlab Enumerator.new do |yielder| @lines.each do |line| next if filename?(line) - - full_line = line.gsub(/\n/, '') - + + full_line = line.delete("\n") + if line.match(/^@@ -/) type = "match" - + line_old = line.match(/\-[0-9]*/)[0].to_i.abs rescue 0 line_new = line.match(/\+[0-9]*/)[0].to_i.abs rescue 0 - - next if line_old <= 1 && line_new <= 1 #top of file + + next if line_old <= 1 && line_new <= 1 # top of file yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new) line_obj_index += 1 next @@ -39,8 +39,8 @@ module Gitlab yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new) line_obj_index += 1 end - - + + case line[0] when "+" line_new += 1 diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 8f9be6cd9a3..e2fee6b9f3e 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -2,22 +2,21 @@ module Gitlab module Email module Message class RepositoryPush - attr_accessor :recipient attr_reader :author_id, :ref, :action include Gitlab::Routing.url_helpers + include DiffHelper delegate :namespace, :name_with_namespace, to: :project, prefix: :project delegate :name, to: :author, prefix: :author delegate :username, to: :author, prefix: :author - def initialize(notify, project_id, recipient, opts = {}) + def initialize(notify, project_id, opts = {}) raise ArgumentError, 'Missing options: author_id, ref, action' unless opts[:author_id] && opts[:ref] && opts[:action] @notify = notify @project_id = project_id - @recipient = recipient @opts = opts.dup @author_id = @opts.delete(:author_id) @@ -38,7 +37,7 @@ module Gitlab end def diffs - @diffs ||= (compare.diffs if compare) + @diffs ||= (safe_diff_files(compare.diffs, diff_refs) if compare) end def diffs_count @@ -49,6 +48,10 @@ module Gitlab @opts[:compare] end + def diff_refs + @opts[:diff_refs] + end + def compare_timeout diffs.overflow? if diffs end diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb index 6ed36b51f12..3411eb1d9ce 100644 --- a/lib/gitlab/email/reply_parser.rb +++ b/lib/gitlab/email/reply_parser.rb @@ -65,7 +65,7 @@ module Gitlab (l =~ /On \w+ \d+,? \d+,?.*wrote:/) # Headers on subsequent lines - break if (0..2).all? { |off| lines[idx+off] =~ REPLYING_HEADER_REGEX } + break if (0..2).all? { |off| lines[idx + off] =~ REPLYING_HEADER_REGEX } # Headers on the same line break if REPLYING_HEADER_LABELS.count { |label| l.include?(label) } >= 3 diff --git a/lib/gitlab/fogbugz_import/project_creator.rb b/lib/gitlab/fogbugz_import/project_creator.rb index 3840765db87..1918d5b208d 100644 --- a/lib/gitlab/fogbugz_import/project_creator.rb +++ b/lib/gitlab/fogbugz_import/project_creator.rb @@ -12,7 +12,7 @@ module Gitlab end def execute - project = ::Projects::CreateService.new( + ::Projects::CreateService.new( current_user, name: repo.safe_name, path: repo.path, @@ -21,12 +21,9 @@ module Gitlab visibility_level: Gitlab::VisibilityLevel::INTERNAL, import_type: 'fogbugz', import_source: repo.name, - import_url: Project::UNKNOWN_IMPORT_URL + import_url: Project::UNKNOWN_IMPORT_URL, + import_data: { data: { 'repo' => repo.raw_data, 'user_map' => user_map }, credentials: { fb_session: fb_session } } ).execute - - project.create_or_update_import_data(data: { 'repo' => repo.raw_data, 'user_map' => user_map }, credentials: { fb_session: fb_session }) - - project end end end diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb index 202263c6742..72992baffd4 100644 --- a/lib/gitlab/github_import/base_formatter.rb +++ b/lib/gitlab/github_import/base_formatter.rb @@ -9,6 +9,10 @@ module Gitlab @formatter = Gitlab::ImportFormatter.new end + def create! + self.klass.create!(self.attributes) + end + private def gl_user_id(github_id) diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb new file mode 100644 index 00000000000..a15fc84b418 --- /dev/null +++ b/lib/gitlab/github_import/branch_formatter.rb @@ -0,0 +1,29 @@ +module Gitlab + module GithubImport + class BranchFormatter < BaseFormatter + delegate :repo, :sha, :ref, to: :raw_data + + def exists? + project.repository.branch_exists?(ref) + end + + def name + @name ||= exists? ? ref : "#{ref}-#{short_id}" + end + + def valid? + repo.present? + end + + def valid? + repo.present? + end + + private + + def short_id + sha.to_s[0..7] + end + end + end +end diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 67988ea3460..d325eca6d99 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -1,6 +1,9 @@ module Gitlab module GithubImport class Client + GITHUB_SAFE_REMAINING_REQUESTS = 100 + GITHUB_SAFE_SLEEP_TIME = 500 + attr_reader :client, :api def initialize(access_token) @@ -11,7 +14,7 @@ module Gitlab ) if access_token - ::Octokit.auto_paginate = true + ::Octokit.auto_paginate = false @api = ::Octokit::Client.new( access_token: access_token, @@ -36,7 +39,7 @@ module Gitlab def method_missing(method, *args, &block) if api.respond_to?(method) - api.send(method, *args, &block) + request { api.send(method, *args, &block) } else super(method, *args, &block) end @@ -55,6 +58,34 @@ module Gitlab def github_options config["args"]["client_options"].deep_symbolize_keys end + + def rate_limit + api.rate_limit! + end + + def rate_limit_exceed? + rate_limit.remaining <= GITHUB_SAFE_REMAINING_REQUESTS + end + + def rate_limit_sleep_time + rate_limit.resets_in + GITHUB_SAFE_SLEEP_TIME + end + + def request + sleep rate_limit_sleep_time if rate_limit_exceed? + + data = yield + + last_response = api.last_response + + while last_response.rels[:next] + sleep rate_limit_sleep_time if rate_limit_exceed? + last_response = last_response.rels[:next].get + data.concat(last_response.data) if last_response.data.is_a?(Array) + end + + data + end end end end diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb index 7d679eaec6a..2c1b94ef2cd 100644 --- a/lib/gitlab/github_import/comment_formatter.rb +++ b/lib/gitlab/github_import/comment_formatter.rb @@ -8,6 +8,7 @@ module Gitlab commit_id: raw_data.commit_id, line_code: line_code, author_id: author_id, + type: type, created_at: raw_data.created_at, updated_at: raw_data.updated_at } @@ -53,6 +54,10 @@ module Gitlab def note formatter.author_line(author) + body end + + def type + 'LegacyDiffNote' if on_diff? + end end end end diff --git a/lib/gitlab/github_import/hook_formatter.rb b/lib/gitlab/github_import/hook_formatter.rb new file mode 100644 index 00000000000..db1fabaa18a --- /dev/null +++ b/lib/gitlab/github_import/hook_formatter.rb @@ -0,0 +1,23 @@ +module Gitlab + module GithubImport + class HookFormatter + EVENTS = %w[* create delete pull_request push].freeze + + attr_reader :raw + + delegate :id, :name, :active, to: :raw + + def initialize(raw) + @raw = raw + end + + def config + raw.config.attrs + end + + def valid? + (EVENTS & raw.events).any? && active + end + end + end +end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 0f9e3ee14ee..e5cf66a0371 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -3,12 +3,15 @@ module Gitlab class Importer include Gitlab::ShellAdapter - attr_reader :project, :client + attr_reader :client, :project, :repo, :repo_url def initialize(project) - @project = project - if import_data_credentials - @client = Client.new(import_data_credentials[:user]) + @project = project + @repo = project.import_source + @repo_url = project.import_url + + if credentials + @client = Client.new(credentials[:user]) @formatter = Gitlab::ImportFormatter.new else raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" @@ -22,14 +25,13 @@ module Gitlab private - def import_data_credentials - @import_data_credentials ||= project.import_data.credentials if project.import_data + def credentials + @credentials ||= project.import_data.credentials if project.import_data end def import_labels - client.labels(project.import_source).each do |raw_data| - Label.create!(LabelFormatter.new(project, raw_data).attributes) - end + labels = client.labels(repo, per_page: 100) + labels.each { |raw| LabelFormatter.new(project, raw).create! } true rescue ActiveRecord::RecordInvalid => e @@ -37,9 +39,8 @@ module Gitlab end def import_milestones - client.list_milestones(project.import_source, state: :all).each do |raw_data| - Milestone.create!(MilestoneFormatter.new(project, raw_data).attributes) - end + milestones = client.milestones(repo, state: :all, per_page: 100) + milestones.each { |raw| MilestoneFormatter.new(project, raw).create! } true rescue ActiveRecord::RecordInvalid => e @@ -47,18 +48,15 @@ module Gitlab end def import_issues - client.list_issues(project.import_source, state: :all, - sort: :created, - direction: :asc).each do |raw_data| - gh_issue = IssueFormatter.new(project, raw_data) + issues = client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100) - if gh_issue.valid? - issue = Issue.create!(gh_issue.attributes) - apply_labels(gh_issue.number, issue) + issues.each do |raw| + gh_issue = IssueFormatter.new(project, raw) - if gh_issue.has_comments? - import_comments(gh_issue.number, issue) - end + if gh_issue.valid? + issue = gh_issue.create! + apply_labels(issue) + import_comments(issue) if gh_issue.has_comments? end end @@ -68,29 +66,64 @@ module Gitlab end def import_pull_requests - client.pull_requests(project.import_source, state: :all, - sort: :created, - direction: :asc).each do |raw_data| - pull_request = PullRequestFormatter.new(project, raw_data) - - if pull_request.valid? - merge_request = MergeRequest.new(pull_request.attributes) - - if merge_request.save - apply_labels(pull_request.number, merge_request) - import_comments(pull_request.number, merge_request) - import_comments_on_diff(pull_request.number, merge_request) - end - end + hooks = client.hooks(repo).map { |raw| HookFormatter.new(raw) }.select(&:valid?) + disable_webhooks(hooks) + + pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) + pull_requests = pull_requests.map { |raw| PullRequestFormatter.new(project, raw) }.select(&:valid?) + + source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch_name, pr.source_branch_sha] } + target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch_name, pr.target_branch_sha] } + branches_removed = source_branches_removed | target_branches_removed + + restore_branches(branches_removed) + + pull_requests.each do |pull_request| + merge_request = pull_request.create! + apply_labels(merge_request) + import_comments(merge_request) + import_comments_on_diff(merge_request) end true rescue ActiveRecord::RecordInvalid => e raise Projects::ImportService::Error, e.message + ensure + clean_up_restored_branches(branches_removed) + clean_up_disabled_webhooks(hooks) + end + + def disable_webhooks(hooks) + update_webhooks(hooks, active: false) + end + + def clean_up_disabled_webhooks(hooks) + update_webhooks(hooks, active: true) + end + + def update_webhooks(hooks, options) + hooks.each do |hook| + client.edit_hook(repo, hook.id, hook.name, hook.config, options) + end + end + + def restore_branches(branches) + branches.each do |name, sha| + client.create_ref(repo, "refs/heads/#{name}", sha) + end + + project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*') + end + + def clean_up_restored_branches(branches) + branches.each do |name, _| + client.delete_ref(repo, "heads/#{name}") + project.repository.rm_branch(project.creator, name) + end end - def apply_labels(number, issuable) - issue = client.issue(project.import_source, number) + def apply_labels(issuable) + issue = client.issue(repo, issuable.iid) if issue.labels.count > 0 label_ids = issue.labels.map do |raw| @@ -101,20 +134,20 @@ module Gitlab end end - def import_comments(issue_number, noteable) - comments = client.issue_comments(project.import_source, issue_number) - create_comments(comments, noteable) + def import_comments(issuable) + comments = client.issue_comments(repo, issuable.iid, per_page: 100) + create_comments(issuable, comments) end - def import_comments_on_diff(pull_request_number, merge_request) - comments = client.pull_request_comments(project.import_source, pull_request_number) - create_comments(comments, merge_request) + def import_comments_on_diff(merge_request) + comments = client.pull_request_comments(repo, merge_request.iid, per_page: 100) + create_comments(merge_request, comments) end - def create_comments(comments, noteable) - comments.each do |raw_data| - comment = CommentFormatter.new(project, raw_data) - noteable.notes.create!(comment.attributes) + def create_comments(issuable, comments) + comments.each do |raw| + comment = CommentFormatter.new(project, raw) + issuable.notes.create!(comment.attributes) end end diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb index c8173913b4e..835ec858b35 100644 --- a/lib/gitlab/github_import/issue_formatter.rb +++ b/lib/gitlab/github_import/issue_formatter.rb @@ -20,6 +20,10 @@ module Gitlab raw_data.comments > 0 end + def klass + Issue + end + def number raw_data.number end diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb index c2b9d40b511..9f18244e7d7 100644 --- a/lib/gitlab/github_import/label_formatter.rb +++ b/lib/gitlab/github_import/label_formatter.rb @@ -9,6 +9,10 @@ module Gitlab } end + def klass + Label + end + private def color diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb index e91a7e328cf..53d4b3102d1 100644 --- a/lib/gitlab/github_import/milestone_formatter.rb +++ b/lib/gitlab/github_import/milestone_formatter.rb @@ -14,6 +14,10 @@ module Gitlab } end + def klass + Milestone + end + private def number diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index d21b942ad4b..498b00cb658 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -1,15 +1,20 @@ module Gitlab module GithubImport class PullRequestFormatter < BaseFormatter + delegate :exists?, :name, :project, :repo, :sha, to: :source_branch, prefix: true + delegate :exists?, :name, :project, :repo, :sha, to: :target_branch, prefix: true + def attributes { iid: number, title: raw_data.title, description: description, - source_project: source_project, - source_branch: source_branch.name, - target_project: target_project, - target_branch: target_branch.name, + source_project: source_branch_project, + source_branch: source_branch_name, + head_source_sha: source_branch_sha, + target_project: target_branch_project, + target_branch: target_branch_name, + base_target_sha: target_branch_sha, state: state, milestone: milestone, author_id: author_id, @@ -19,12 +24,24 @@ module Gitlab } end + def klass + MergeRequest + end + def number raw_data.number end def valid? - !cross_project? && source_branch.present? && target_branch.present? + source_branch.valid? && target_branch.valid? && !cross_project? + end + + def source_branch + @source_branch ||= BranchFormatter.new(project, raw_data.head) + end + + def target_branch + @target_branch ||= BranchFormatter.new(project, raw_data.base) end private @@ -52,7 +69,7 @@ module Gitlab end def cross_project? - source_repo.present? && target_repo.present? && source_repo.id != target_repo.id + source_branch_repo.id != target_branch_repo.id end def description @@ -65,35 +82,10 @@ module Gitlab end end - def source_project - project - end - - def source_repo - raw_data.head.repo - end - - def source_branch - source_project.repository.find_branch(raw_data.head.ref) - end - - def target_project - project - end - - def target_repo - raw_data.base.repo - end - - def target_branch - target_project.repository.find_branch(raw_data.base.ref) - end - def state - @state ||= case true - when raw_data.state == 'closed' && raw_data.merged_at.present? + @state ||= if raw_data.state == 'closed' && raw_data.merged_at.present? 'merged' - when raw_data.state == 'closed' + elsif raw_data.state == 'closed' 'closed' else 'opened' diff --git a/lib/gitlab/gitignore.rb b/lib/gitlab/gitignore.rb new file mode 100644 index 00000000000..f46b43b61a4 --- /dev/null +++ b/lib/gitlab/gitignore.rb @@ -0,0 +1,56 @@ +module Gitlab + class Gitignore + FILTER_REGEX = /\.gitignore\z/.freeze + + def initialize(path) + @path = path + end + + def name + File.basename(@path, '.gitignore') + end + + def content + File.read(@path) + end + + class << self + def all + languages_frameworks + global + end + + def find(key) + file_name = "#{key}.gitignore" + + directory = select_directory(file_name) + directory ? new(File.join(directory, file_name)) : nil + end + + def global + files_for_folder(global_dir).map { |file| new(File.join(global_dir, file)) } + end + + def languages_frameworks + files_for_folder(gitignore_dir).map { |file| new(File.join(gitignore_dir, file)) } + end + + private + + def select_directory(file_name) + [gitignore_dir, global_dir].find { |dir| File.exist?(File.join(dir, file_name)) } + end + + def global_dir + File.join(gitignore_dir, 'Global') + end + + def gitignore_dir + Rails.root.join('vendor/gitignore') + end + + def files_for_folder(dir) + Dir.glob("#{dir.to_s}/*.gitignore").map { |file| file.gsub(FILTER_REGEX, '') } + end + end + end +end diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb index 96717b42bae..3f76ec97977 100644 --- a/lib/gitlab/gitlab_import/importer.rb +++ b/lib/gitlab/gitlab_import/importer.rb @@ -5,9 +5,9 @@ module Gitlab def initialize(project) @project = project - credentials = import_data - if credentials && credentials[:password] - @client = Client.new(credentials[:password]) + import_data = project.import_data + if import_data && import_data.credentials && import_data.credentials[:password] + @client = Client.new(import_data.credentials[:password]) @formatter = Gitlab::ImportFormatter.new else raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" @@ -17,7 +17,7 @@ module Gitlab def execute project_identifier = CGI.escape(project.import_source) - #Issues && Comments + # Issues && Comments issues = client.issues(project_identifier) issues.each do |issue| diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index ab900b641c4..f751a3a12fd 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -8,6 +8,7 @@ module Gitlab gon.relative_url_root = Gitlab.config.gitlab.relative_url_root gon.shortcuts_path = help_shortcuts_path gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class + gon.award_menu_url = emojis_path if current_user gon.current_user_id = current_user.id diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb index 0abb7a64c17..326cfcaa8af 100644 --- a/lib/gitlab/google_code_import/project_creator.rb +++ b/lib/gitlab/google_code_import/project_creator.rb @@ -11,7 +11,7 @@ module Gitlab end def execute - project = ::Projects::CreateService.new( + ::Projects::CreateService.new( current_user, name: repo.name, path: repo.name, @@ -21,12 +21,9 @@ module Gitlab visibility_level: Gitlab::VisibilityLevel::PUBLIC, import_type: "google_code", import_source: repo.name, - import_url: repo.import_url + import_url: repo.import_url, + import_data: { data: { 'repo' => repo.raw_data, 'user_map' => user_map } } ).execute - - project.create_or_update_import_data(data: { 'repo' => repo.raw_data, 'user_map' => user_map }) - - project end end end diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb index 9c04a3d6995..624c1766024 100644 --- a/lib/gitlab/import_export.rb +++ b/lib/gitlab/import_export.rb @@ -20,6 +20,10 @@ module Gitlab "project.bundle" end + def config_file + 'lib/gitlab/import_export/import_export.yml' + end + def version_filename 'VERSION' end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index eef4d92beee..3796fc8cd02 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -5,7 +5,9 @@ project_tree: :author - :labels - :milestones - - :snippets + - snippets: + - notes: + :author - :releases - :events - project_members: @@ -14,7 +16,9 @@ project_tree: - notes: :author - :merge_request_diff - - ci_commits: + - pipelines: + - notes: + :author - :statuses - :variables - :triggers diff --git a/lib/gitlab/import_export/import_export_reader.rb b/lib/gitlab/import_export/import_export_reader.rb index d42005b2bfd..25253330745 100644 --- a/lib/gitlab/import_export/import_export_reader.rb +++ b/lib/gitlab/import_export/import_export_reader.rb @@ -4,17 +4,19 @@ module Gitlab attr_reader :tree - def initialize(config: 'lib/gitlab/import_export/import_export.yml', shared:) + def initialize(shared:) @shared = shared - config_hash = YAML.load_file(config).deep_symbolize_keys + config_hash = YAML.load_file(Gitlab::ImportExport.config_file).deep_symbolize_keys @tree = config_hash[:project_tree] - @attributes_parser = Gitlab::ImportExport::AttributesFinder.new(included_attributes: config_hash[:included_attributes], + @attributes_finder = Gitlab::ImportExport::AttributesFinder.new(included_attributes: config_hash[:included_attributes], excluded_attributes: config_hash[:excluded_attributes], methods: config_hash[:methods]) end + # Outputs a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html + # for outputting a project in JSON format, including its relations and sub relations. def project_tree - @attributes_parser.find_included(:project).merge(include: build_hash(@tree)) + @attributes_finder.find_included(:project).merge(include: build_hash(@tree)) rescue => e @shared.error(e) false @@ -22,23 +24,30 @@ module Gitlab private + # Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html + # + # +model_list+ - List of models as a relation tree to be included in the generated JSON, from the _import_export.yml_ file def build_hash(model_list) model_list.map do |model_objects| if model_objects.is_a?(Hash) build_json_config_hash(model_objects) else - @attributes_parser.find(model_objects) + @attributes_finder.find(model_objects) end end end + # Called when the model is actually a hash containing other relations (more models) + # Returns the config in the right format for calling +to_json+ + # +model_object_hash+ - A model relationship such as: + # {:merge_requests=>[:merge_request_diff, :notes]} def build_json_config_hash(model_object_hash) @json_config_hash = {} model_object_hash.values.flatten.each do |model_object| current_key = model_object_hash.keys.first - @attributes_parser.parse(current_key) { |hash| @json_config_hash[current_key] ||= hash } + @attributes_finder.parse(current_key) { |hash| @json_config_hash[current_key] ||= hash } handle_model_object(current_key, model_object) process_sub_model(current_key, model_object) if model_object.is_a?(Hash) @@ -46,6 +55,11 @@ module Gitlab @json_config_hash end + + # If the model is a hash, process the sub_models, which could also be hashes + # If there is a list, add to an existing array, otherwise use hash syntax + # +current_key+ main model that will be a key in the hash + # +model_object+ model or list of models to include in the hash def process_sub_model(current_key, model_object) sub_model_json = build_json_config_hash(model_object).dup @json_config_hash.slice!(current_key) @@ -57,6 +71,9 @@ module Gitlab end end + # Creates or adds to an existing hash an individual model or list + # +current_key+ main model that will be a key in the hash + # +model_object+ model or list of models to include in the hash def handle_model_object(current_key, model_object) if @json_config_hash[current_key] add_model_value(current_key, model_object) @@ -65,21 +82,33 @@ module Gitlab end end + # Constructs a new hash that will hold the configuration for that particular object + # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+ + # +current_key+ main model that will be a key in the hash + # +value+ existing model to be included in the hash def create_model_value(current_key, value) parsed_hash = { include: value } - @attributes_parser.parse(value) do |hash| + @attributes_finder.parse(value) do |hash| parsed_hash = { include: hash_or_merge(value, hash) } end @json_config_hash[current_key] = parsed_hash end + # Adds new model configuration to an existing hash with key +current_key+ + # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+ + # +current_key+ main model that will be a key in the hash + # +value+ existing model to be included in the hash def add_model_value(current_key, value) - @attributes_parser.parse(value) { |hash| value = { value => hash } } + @attributes_finder.parse(value) { |hash| value = { value => hash } } old_values = @json_config_hash[current_key][:include] @json_config_hash[current_key][:include] = ([old_values] + [value]).compact.flatten end + # Construct a new hash or merge with an existing one a model configuration + # This is to fulfil +to_json+ requirements. + # +value+ existing model to be included in the hash + # +hash+ hash containing configuration generated mainly from +@attributes_finder+ def hash_or_merge(value, hash) value.is_a?(Hash) ? value.merge(hash) : { value => hash } end diff --git a/lib/gitlab/import_export/repo_bundler.rb b/lib/gitlab/import_export/repo_saver.rb index 377dfcfe2c9..07d71c78f33 100644 --- a/lib/gitlab/import_export/repo_bundler.rb +++ b/lib/gitlab/import_export/repo_saver.rb @@ -1,6 +1,6 @@ module Gitlab module ImportExport - class RepoBundler + class RepoSaver include Gitlab::ImportExport::CommandLineUtil attr_reader :full_path @@ -10,7 +10,7 @@ module Gitlab @shared = shared end - def bundle + def save return false if @project.empty_repo? @full_path = File.join(@shared.export_path, ImportExport.project_bundle_filename) bundle_to_disk diff --git a/lib/gitlab/import_export/version_saver.rb b/lib/gitlab/import_export/version_saver.rb index 59e317e6573..fe02a2a6d76 100644 --- a/lib/gitlab/import_export/version_saver.rb +++ b/lib/gitlab/import_export/version_saver.rb @@ -2,10 +2,6 @@ module Gitlab module ImportExport class VersionSaver - def self.save(*args) - new(*args).save - end - def initialize(shared:) @shared = shared end diff --git a/lib/gitlab/import_export/wiki_repo_bundler.rb b/lib/gitlab/import_export/wiki_repo_saver.rb index 015f54d17af..33c91fd7ba5 100644 --- a/lib/gitlab/import_export/wiki_repo_bundler.rb +++ b/lib/gitlab/import_export/wiki_repo_saver.rb @@ -1,7 +1,7 @@ module Gitlab module ImportExport - class WikiRepoBundler < RepoBundler - def bundle + class WikiRepoSaver < RepoSaver + def save @wiki = ProjectWiki.new(@project) return true unless wiki_repository_exists? # it's okay to have no Wiki bundle_to_disk(File.join(@shared.export_path, project_filename)) diff --git a/lib/gitlab/key_fingerprint.rb b/lib/gitlab/key_fingerprint.rb index baf52ff750d..8684b4636ea 100644 --- a/lib/gitlab/key_fingerprint.rb +++ b/lib/gitlab/key_fingerprint.rb @@ -17,9 +17,9 @@ module Gitlab file.rewind cmd = [] - cmd.push *%W(ssh-keygen) - cmd.push *%W(-E md5) if explicit_fingerprint_algorithm? - cmd.push *%W(-lf #{file.path}) + cmd.push('ssh-keygen') + cmd.push('-E', 'md5') if explicit_fingerprint_algorithm? + cmd.push('-lf', file.path) cmd_output, cmd_status = popen(cmd, '/tmp') end diff --git a/lib/gitlab/lazy.rb b/lib/gitlab/lazy.rb new file mode 100644 index 00000000000..2a659ae4c74 --- /dev/null +++ b/lib/gitlab/lazy.rb @@ -0,0 +1,34 @@ +module Gitlab + # A class that can be wrapped around an expensive method call so it's only + # executed when actually needed. + # + # Usage: + # + # object = Gitlab::Lazy.new { some_expensive_work_here } + # + # object['foo'] + # object.bar + class Lazy < BasicObject + def initialize(&block) + @block = block + end + + def method_missing(name, *args, &block) + __evaluate__ + + @result.__send__(name, *args, &block) + end + + def respond_to_missing?(name, include_private = false) + __evaluate__ + + @result.respond_to?(name, include_private) || super + end + + private + + def __evaluate__ + @result = @block.call unless defined?(@result) + end + end +end diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index aff7ccb157f..f9bb5775323 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -93,6 +93,7 @@ module Gitlab end protected + def base_config Gitlab.config.ldap end diff --git a/lib/gitlab/markup_helper.rb b/lib/gitlab/markup_helper.rb index a5f767b134d..dda371e6554 100644 --- a/lib/gitlab/markup_helper.rb +++ b/lib/gitlab/markup_helper.rb @@ -40,7 +40,7 @@ module Gitlab # Returns boolean def plain?(filename) filename.downcase.end_with?('.txt') || - filename.downcase == 'readme' + filename.casecmp('readme').zero? end def previewable?(filename) diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb index 708ef79f304..0f115893a15 100644 --- a/lib/gitlab/metrics/instrumentation.rb +++ b/lib/gitlab/metrics/instrumentation.rb @@ -154,8 +154,6 @@ module Gitlab duration = (Time.now - start) * 1000.0 if duration >= Gitlab::Metrics.method_call_threshold - trans.increment(:method_duration, duration) - trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES, { duration: duration }, method: #{label.inspect}) diff --git a/lib/gitlab/metrics/subscribers/rails_cache.rb b/lib/gitlab/metrics/subscribers/rails_cache.rb index 49e5f86e6e6..8e345e8ae4a 100644 --- a/lib/gitlab/metrics/subscribers/rails_cache.rb +++ b/lib/gitlab/metrics/subscribers/rails_cache.rb @@ -6,26 +6,28 @@ module Gitlab attach_to :active_support def cache_read(event) - increment(:cache_read_duration, event.duration) + increment(:cache_read, event.duration) end def cache_write(event) - increment(:cache_write_duration, event.duration) + increment(:cache_write, event.duration) end def cache_delete(event) - increment(:cache_delete_duration, event.duration) + increment(:cache_delete, event.duration) end def cache_exist?(event) - increment(:cache_exists_duration, event.duration) + increment(:cache_exists, event.duration) end def increment(key, duration) return unless current_transaction current_transaction.increment(:cache_duration, duration) - current_transaction.increment(key, duration) + current_transaction.increment(:cache_count, 1) + current_transaction.increment("#{key}_duration".to_sym, duration) + current_transaction.increment("#{key}_count".to_sym, 1) end private diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb index 50b0dd32380..5764ab15652 100644 --- a/lib/gitlab/middleware/go.rb +++ b/lib/gitlab/middleware/go.rb @@ -39,7 +39,7 @@ module Gitlab request_url = URI.join(base_url, project_path) domain_path = strip_url(request_url.to_s) - "<!DOCTYPE html><html><head><meta content='#{domain_path} git #{request_url}.git' name='go-import'></head></html>\n"; + "<!DOCTYPE html><html><head><meta content='#{domain_path} git #{request_url}.git' name='go-import'></head></html>\n" end def strip_url(url) diff --git a/lib/gitlab/middleware/rails_queue_duration.rb b/lib/gitlab/middleware/rails_queue_duration.rb new file mode 100644 index 00000000000..56608b1b276 --- /dev/null +++ b/lib/gitlab/middleware/rails_queue_duration.rb @@ -0,0 +1,24 @@ +# This Rack middleware is intended to measure the latency between +# gitlab-workhorse forwarding a request to the Rails application and the +# time this middleware is reached. + +module Gitlab + module Middleware + class RailsQueueDuration + def initialize(app) + @app = app + end + + def call(env) + trans = Gitlab::Metrics.current_transaction + proxy_start = env['HTTP_GITLAB_WORHORSE_PROXY_START'].presence + if trans && proxy_start + # Time in milliseconds since gitlab-workhorse started the request + trans.set(:rails_queue_duration, Time.now.to_f * 1_000 - proxy_start.to_f / 1_000_000) + end + + @app.call(env) + end + end + end +end diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index 356e96fcbab..78f3ecb4cb4 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -69,13 +69,20 @@ module Gitlab return unless ldap_person # If a corresponding person exists with same uid in a LDAP server, - # set up a Gitlab user with dual LDAP and Omniauth identities. - if user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider) - # Case when a LDAP user already exists in Gitlab. Add the Omniauth identity to existing account. + # check if the user already has a GitLab account. + user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider) + if user + # Case when a LDAP user already exists in Gitlab. Add the OAuth identity to existing account. + log.info "LDAP account found for user #{user.username}. Building new #{auth_hash.provider} identity." user.identities.build(extern_uid: auth_hash.uid, provider: auth_hash.provider) else - # No account in Gitlab yet: create it and add the LDAP identity - user = build_new_user + log.info "No existing LDAP account was found in GitLab. Checking for #{auth_hash.provider} account." + user = find_by_uid_and_provider + if user.nil? + log.info "No user found using #{auth_hash.provider} provider. Creating a new one." + user = build_new_user + end + log.info "Correct account has been found. Adding LDAP identity to user: #{user.username}." user.identities.new(provider: ldap_person.provider, extern_uid: ldap_person.dn) end diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 71c5b6801fb..183bd10d6a3 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -74,7 +74,7 @@ module Gitlab end def notes - project.notes.user.search(query).order('updated_at DESC') + project.notes.user.search(query, as_user: @current_user).order('updated_at DESC') end def commits diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index 5c352c96de5..40766f35f77 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -25,7 +25,7 @@ module Gitlab end @pool.with { |redis| yield redis } end - + def self.redis_store_options url = new.url redis_config_hash = ::Redis::Store::Factory.extract_host_options_from_uri(url) @@ -40,10 +40,10 @@ module Gitlab def initialize(rails_env=nil) rails_env ||= Rails.env config_file = File.expand_path('../../../config/resque.yml', __FILE__) - + @url = "redis://localhost:6379" - if File.exists?(config_file) - @url =YAML.load_file(config_file)[rails_env] + if File.exist?(config_file) + @url = YAML.load_file(config_file)[rails_env] end end end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 13c4d64c99b..11c0b01f0dc 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -4,10 +4,9 @@ module Gitlab REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range) attr_accessor :project, :current_user, :author - def initialize(project, current_user = nil, author = nil) + def initialize(project, current_user = nil) @project = project @current_user = current_user - @author = author @references = {} @@ -18,17 +17,21 @@ module Gitlab super(text, context.merge(project: project)) end + def references(type) + super(type, project, current_user) + end + REFERABLES.each do |type| define_method("#{type}s") do - @references[type] ||= references(type, reference_context) + @references[type] ||= references(type) end end def issues if project && project.jira_tracker? - @references[:external_issue] ||= references(:external_issue, reference_context) + @references[:external_issue] ||= references(:external_issue) else - @references[:issue] ||= references(:issue, reference_context) + @references[:issue] ||= references(:issue) end end @@ -46,11 +49,5 @@ module Gitlab @pattern = Regexp.union(patterns.compact) end - - private - - def reference_context - { project: project, current_user: current_user, author: author } - end end end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index ace906a6f59..1cbd6d945a0 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -96,5 +96,9 @@ module Gitlab (?<![\/.]) (?# rule #6-7) }x.freeze end + + def container_registry_reference_regex + git_reference_regex + end end end diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb index dba4bbfc899..8943022612c 100644 --- a/lib/gitlab/saml/user.rb +++ b/lib/gitlab/saml/user.rb @@ -12,12 +12,12 @@ module Gitlab end def gl_user - @user ||= find_by_uid_and_provider - if auto_link_ldap_user? @user ||= find_or_create_ldap_user end + @user ||= find_by_uid_and_provider + if auto_link_saml_user? @user ||= find_by_email end diff --git a/lib/gitlab/sanitizers/svg.rb b/lib/gitlab/sanitizers/svg.rb index b98589dff89..8304b9a482c 100644 --- a/lib/gitlab/sanitizers/svg.rb +++ b/lib/gitlab/sanitizers/svg.rb @@ -1,5 +1,3 @@ -require_relative "svg/whitelist" - module Gitlab module Sanitizers module SVG @@ -12,25 +10,47 @@ module Gitlab DATA_ATTR_PATTERN = /\Adata-(?!xml)[a-z_][\w.\u00E0-\u00F6\u00F8-\u017F\u01DD-\u02AF-]*\z/u def scrub(node) - unless ALLOWED_ELEMENTS.include?(node.name) + unless Whitelist::ALLOWED_ELEMENTS.include?(node.name) node.unlink - else - node.attributes.each do |attr_name, attr| - valid_attributes = ALLOWED_ATTRIBUTES[node.name] - - unless valid_attributes && valid_attributes.include?(attr_name) - if ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS.include?(node.name) && - attr_name.start_with?('data-') - # Arbitrary data attributes are allowed. Verify that the attribute - # is a valid data attribute. - attr.unlink unless attr_name =~ DATA_ATTR_PATTERN - else - attr.unlink - end + return + end + + valid_attributes = Whitelist::ALLOWED_ATTRIBUTES[node.name] + return unless valid_attributes + + node.attribute_nodes.each do |attr| + attr_name = attribute_name_with_namespace(attr) + + if valid_attributes.include?(attr_name) + attr.unlink if unsafe_href?(attr) + else + # Arbitrary data attributes are allowed. + unless allows_data_attribute?(node) && data_attribute?(attr) + attr.unlink end end end end + + def attribute_name_with_namespace(attr) + if attr.namespace + "#{attr.namespace.prefix}:#{attr.name}" + else + attr.name + end + end + + def allows_data_attribute?(node) + Whitelist::ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS.include?(node.name) + end + + def unsafe_href?(attr) + attribute_name_with_namespace(attr) == 'xlink:href' && !attr.value.start_with?('#') + end + + def data_attribute?(attr) + attr.name.start_with?('data-') && attr.name =~ DATA_ATTR_PATTERN && attr.namespace.nil? + end end end end diff --git a/lib/gitlab/sanitizers/svg/whitelist.rb b/lib/gitlab/sanitizers/svg/whitelist.rb index 917e795b29e..7b6b70d8dbc 100644 --- a/lib/gitlab/sanitizers/svg/whitelist.rb +++ b/lib/gitlab/sanitizers/svg/whitelist.rb @@ -4,7 +4,8 @@ module Gitlab module Sanitizers module SVG - ALLOWED_ELEMENTS = %w[ + class Whitelist + ALLOWED_ELEMENTS = %w[ a altGlyph altGlyphDef altGlyphItem animate animateColor animateMotion animateTransform circle clipPath color-profile cursor defs desc ellipse feBlend feColorMatrix feComponentTransfer @@ -18,90 +19,91 @@ module Gitlab script set stop style svg switch symbol text textPath title tref tspan use view vkern].freeze - ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS = %w[svg].freeze + ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS = %w[svg].freeze - ALLOWED_ATTRIBUTES = { - 'a' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage target text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'altGlyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'altGlyphDef' => %w[id xml:base xml:lang xml:space], - 'altGlyphItem' => %w[id xml:base xml:lang xml:space], - 'animate' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'animateColor' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'animateMotion' => %w[accumulate additive begin by calcMode dur end externalResourcesRequired fill from id keyPoints keySplines keyTimes max min onbegin onend onload onrepeat origin path repeatCount repeatDur requiredExtensions requiredFeatures restart rotate systemLanguage to values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'animateTransform' => %w[accumulate additive attributeName attributeType begin by calcMode dur end externalResourcesRequired fill from id keySplines keyTimes max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to type values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'circle' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events r requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'clipPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule clipPathUnits color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'color-profile' => %w[id local name rendering-intent xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'cursor' => %w[externalResourcesRequired id requiredExtensions requiredFeatures systemLanguage x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'defs' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'desc' => %w[class id style xml:base xml:lang xml:space], - 'ellipse' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'feBlend' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask mode opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feColorMatrix' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi values visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feComponentTransfer' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feComposite' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 k1 k2 k3 k4 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feConvolveMatrix' => %w[alignment-baseline baseline-shift bias class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display divisor dominant-baseline edgeMode enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelMatrix kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity order overflow pointer-events preserveAlpha result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style targetX targetY text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feDiffuseLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor diffuseConstant direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feDisplacementMap' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result scale shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xChannelSelector xml:base xml:lang xml:space y yChannelSelector], - 'feDistantLight' => %w[azimuth elevation id xml:base xml:lang xml:space], - 'feFlood' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feFuncA' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], - 'feFuncB' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], - 'feFuncG' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], - 'feFuncR' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], - 'feGaussianBlur' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stdDeviation stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feImage' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events preserveAspectRatio result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'feMerge' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feMergeNode' => %w[id xml:base xml:lang xml:space], - 'feMorphology' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events radius result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feOffset' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'fePointLight' => %w[id x xml:base xml:lang xml:space y z], - 'feSpecularLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering specularConstant specularExponent stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feSpotLight' => %w[id limitingConeAngle pointsAtX pointsAtY pointsAtZ specularExponent x xml:base xml:lang xml:space y z], - 'feTile' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'feTurbulence' => %w[alignment-baseline baseFrequency baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask numOctaves opacity overflow pointer-events result seed shape-rendering stitchTiles stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'filter' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter filterRes filterUnits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events primitiveUnits shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'font' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x horiz-origin-y id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'font-face' => %w[accent-height alphabetic ascent bbox cap-height descent font-family font-size font-stretch font-style font-variant font-weight hanging id ideographic mathematical overline-position overline-thickness panose-1 slope stemh stemv strikethrough-position strikethrough-thickness underline-position underline-thickness unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical widths x-height xml:base xml:lang xml:space], - 'font-face-format' => %w[id string xml:base xml:lang xml:space], - 'font-face-name' => %w[id name xml:base xml:lang xml:space], - 'font-face-src' => %w[id xml:base xml:lang xml:space], - 'font-face-uri' => %w[id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'foreignObject' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'g' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'glyph' => %w[alignment-baseline arabic-form baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning lang letter-spacing lighting-color marker-end marker-mid marker-start mask opacity orientation overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'glyphRef' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'hkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space], - 'image' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'line' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode x1 x2 xml:base xml:lang xml:space y1 y2], - 'linearGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x1 x2 xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y1 y2], - 'marker' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start markerHeight markerUnits markerWidth mask opacity orient overflow pointer-events preserveAspectRatio refX refY shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'mask' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask maskContentUnits maskUnits opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'metadata' => %w[id xml:base xml:lang xml:space], - 'missing-glyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'mpath' => %w[externalResourcesRequired id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'path' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pathLength pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'pattern' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow patternContentUnits patternTransform patternUnits pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi viewBox visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'polygon' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'polyline' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'radialGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight fx fy glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events r shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space], - 'rect' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], - 'script' => %w[externalResourcesRequired id type xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'set' => %w[attributeName attributeType begin dur end externalResourcesRequired fill id max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], - 'stop' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask offset opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'style' => %w[id media title type xml:base xml:lang xml:space], - 'svg' => %w[alignment-baseline baseProfile baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering contentScriptType contentStyleType cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onabort onactivate onclick onerror onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup onresize onscroll onunload onzoom opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi version viewBox visibility width word-spacing writing-mode x xml:base xml:lang xml:space xmlns y zoomAndPan], - 'switch' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'symbol' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space], - 'text' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength transform unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y], - 'textPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask method onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering spacing startOffset stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space], - 'title' => %w[class id style xml:base xml:lang xml:space], - 'tref' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y], - 'tspan' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y], - 'use' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], - 'view' => %w[externalResourcesRequired id preserveAspectRatio viewBox viewTarget xml:base xml:lang xml:space zoomAndPan], - 'vkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space] - }.freeze + ALLOWED_ATTRIBUTES = { + 'a' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage target text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'altGlyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'altGlyphDef' => %w[id xml:base xml:lang xml:space], + 'altGlyphItem' => %w[id xml:base xml:lang xml:space], + 'animate' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'animateColor' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'animateMotion' => %w[accumulate additive begin by calcMode dur end externalResourcesRequired fill from id keyPoints keySplines keyTimes max min onbegin onend onload onrepeat origin path repeatCount repeatDur requiredExtensions requiredFeatures restart rotate systemLanguage to values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'animateTransform' => %w[accumulate additive attributeName attributeType begin by calcMode dur end externalResourcesRequired fill from id keySplines keyTimes max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to type values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'circle' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events r requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'clipPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule clipPathUnits color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'color-profile' => %w[id local name rendering-intent xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'cursor' => %w[externalResourcesRequired id requiredExtensions requiredFeatures systemLanguage x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'defs' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'desc' => %w[class id style xml:base xml:lang xml:space], + 'ellipse' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'feBlend' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask mode opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feColorMatrix' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi values visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feComponentTransfer' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feComposite' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 k1 k2 k3 k4 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feConvolveMatrix' => %w[alignment-baseline baseline-shift bias class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display divisor dominant-baseline edgeMode enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelMatrix kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity order overflow pointer-events preserveAlpha result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style targetX targetY text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feDiffuseLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor diffuseConstant direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feDisplacementMap' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result scale shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xChannelSelector xml:base xml:lang xml:space y yChannelSelector], + 'feDistantLight' => %w[azimuth elevation id xml:base xml:lang xml:space], + 'feFlood' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feFuncA' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], + 'feFuncB' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], + 'feFuncG' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], + 'feFuncR' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], + 'feGaussianBlur' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stdDeviation stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feImage' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events preserveAspectRatio result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'feMerge' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feMergeNode' => %w[id xml:base xml:lang xml:space], + 'feMorphology' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events radius result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feOffset' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'fePointLight' => %w[id x xml:base xml:lang xml:space y z], + 'feSpecularLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering specularConstant specularExponent stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feSpotLight' => %w[id limitingConeAngle pointsAtX pointsAtY pointsAtZ specularExponent x xml:base xml:lang xml:space y z], + 'feTile' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feTurbulence' => %w[alignment-baseline baseFrequency baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask numOctaves opacity overflow pointer-events result seed shape-rendering stitchTiles stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'filter' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter filterRes filterUnits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events primitiveUnits shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'font' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x horiz-origin-y id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'font-face' => %w[accent-height alphabetic ascent bbox cap-height descent font-family font-size font-stretch font-style font-variant font-weight hanging id ideographic mathematical overline-position overline-thickness panose-1 slope stemh stemv strikethrough-position strikethrough-thickness underline-position underline-thickness unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical widths x-height xml:base xml:lang xml:space], + 'font-face-format' => %w[id string xml:base xml:lang xml:space], + 'font-face-name' => %w[id name xml:base xml:lang xml:space], + 'font-face-src' => %w[id xml:base xml:lang xml:space], + 'font-face-uri' => %w[id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'foreignObject' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'g' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'glyph' => %w[alignment-baseline arabic-form baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning lang letter-spacing lighting-color marker-end marker-mid marker-start mask opacity orientation overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'glyphRef' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'hkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space], + 'image' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'line' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode x1 x2 xml:base xml:lang xml:space y1 y2], + 'linearGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x1 x2 xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y1 y2], + 'marker' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start markerHeight markerUnits markerWidth mask opacity orient overflow pointer-events preserveAspectRatio refX refY shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'mask' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask maskContentUnits maskUnits opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'metadata' => %w[id xml:base xml:lang xml:space], + 'missing-glyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'mpath' => %w[externalResourcesRequired id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'path' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pathLength pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'pattern' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow patternContentUnits patternTransform patternUnits pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi viewBox visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'polygon' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'polyline' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'radialGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight fx fy glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events r shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space], + 'rect' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'script' => %w[externalResourcesRequired id type xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'set' => %w[attributeName attributeType begin dur end externalResourcesRequired fill id max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'stop' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask offset opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'style' => %w[id media title type xml:base xml:lang xml:space], + 'svg' => %w[alignment-baseline baseProfile baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering contentScriptType contentStyleType cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onabort onactivate onclick onerror onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup onresize onscroll onunload onzoom opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi version viewBox visibility width word-spacing writing-mode x xml:base xml:lang xml:space xmlns y zoomAndPan], + 'switch' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'symbol' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'text' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength transform unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y], + 'textPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask method onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering spacing startOffset stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space], + 'title' => %w[class id style xml:base xml:lang xml:space], + 'tref' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y], + 'tspan' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y], + 'use' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'view' => %w[externalResourcesRequired id preserveAspectRatio viewBox viewTarget xml:base xml:lang xml:space zoomAndPan], + 'vkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space] + }.freeze + end end end end diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb index 2ef0e982256..7cf506ebe64 100644 --- a/lib/gitlab/seeder.rb +++ b/lib/gitlab/seeder.rb @@ -5,7 +5,7 @@ module Gitlab SeedFu.quiet = true yield SeedFu.quiet = false - puts "\nOK".green + puts "\nOK".color(:green) end def self.by_user(user) diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 2bbbd3074e8..fe65c246101 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -62,7 +62,7 @@ module Gitlab end def wiki_page_url - "#{Gitlab.config.gitlab.url}#{object.wiki.wiki_base_path}/#{object.slug}" + namespace_project_wiki_url(object.wiki.project.namespace, object.wiki.project, object.slug) end end end diff --git a/lib/gitlab/import_url.rb b/lib/gitlab/url_sanitizer.rb index d23b013c1f5..7d02fe3c971 100644 --- a/lib/gitlab/import_url.rb +++ b/lib/gitlab/url_sanitizer.rb @@ -1,7 +1,13 @@ module Gitlab - class ImportUrl + class UrlSanitizer + def self.sanitize(content) + regexp = URI::Parser.new.make_regexp(['http', 'https', 'ssh', 'git']) + + content.gsub(regexp) { |url| new(url).masked_url } + end + def initialize(url, credentials: nil) - @url = URI.parse(URI.encode(url)) + @url = Addressable::URI.parse(url) @credentials = credentials end @@ -9,6 +15,13 @@ module Gitlab @sanitized_url ||= safe_url.to_s end + def masked_url + url = @url.dup + url.password = "*****" unless url.password.nil? + url.user = "*****" unless url.user.nil? + url.to_s + end + def credentials @credentials ||= { user: @url.user, password: @url.password } end diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index a1ee1cba216..9462f3368e6 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -32,6 +32,13 @@ module Gitlab } end + def highest_allowed_level + restricted_levels = current_application_settings.restricted_visibility_levels + + allowed_levels = self.values - restricted_levels + allowed_levels.max || PRIVATE + end + def allowed_for?(user, level) user.is_admin? || allowed_level?(level.to_i) end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index c3ddd4c2680..388f84dbe0e 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -6,6 +6,13 @@ module Gitlab SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data' class << self + def git_http_ok(repository, user) + { + 'GL_ID' => Gitlab::ShellEnv.gl_id(user), + 'RepoPath' => repository.path_to_repo, + } + end + def send_git_blob(repository, blob) params = { 'RepoPath' => repository.path_to_repo, @@ -14,24 +21,39 @@ module Gitlab [ SEND_DATA_HEADER, - "git-blob:#{encode(params)}", + "git-blob:#{encode(params)}" ] end - def send_git_archive(project, ref, format) + def send_git_archive(repository, ref:, format:) format ||= 'tar.gz' format.downcase! - params = project.repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format) + params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format) raise "Repository or ref not found" if params.empty? [ SEND_DATA_HEADER, - "git-archive:#{encode(params)}", + "git-archive:#{encode(params)}" ] end - + + def send_git_diff(repository, diff_refs) + from, to = diff_refs + + params = { + 'RepoPath' => repository.path_to_repo, + 'ShaFrom' => from.sha, + 'ShaTo' => to.sha + } + + [ + SEND_DATA_HEADER, + "git-diff:#{encode(params)}" + ] + end + protected - + def encode(hash) Base64.urlsafe_encode64(JSON.dump(hash)) end diff --git a/lib/json_web_token/rsa_token.rb b/lib/json_web_token/rsa_token.rb new file mode 100644 index 00000000000..d6d6af7089c --- /dev/null +++ b/lib/json_web_token/rsa_token.rb @@ -0,0 +1,42 @@ +module JSONWebToken + class RSAToken < Token + attr_reader :key_file + + def initialize(key_file) + super() + @key_file = key_file + end + + def encoded + headers = { + kid: kid + } + JWT.encode(payload, key, 'RS256', headers) + end + + private + + def key_data + @key_data ||= File.read(key_file) + end + + def key + @key ||= OpenSSL::PKey::RSA.new(key_data) + end + + def public_key + key.public_key + end + + def kid + # calculate sha256 from DER encoded ASN1 + kid = Digest::SHA256.digest(public_key.to_der) + + # we encode only 30 bytes with base32 + kid = Base32.encode(kid[0..29]) + + # insert colon every 4 characters + kid.scan(/.{4}/).join(':') + end + end +end diff --git a/lib/json_web_token/token.rb b/lib/json_web_token/token.rb new file mode 100644 index 00000000000..5b67715b0b2 --- /dev/null +++ b/lib/json_web_token/token.rb @@ -0,0 +1,46 @@ +module JSONWebToken + class Token + attr_accessor :issuer, :subject, :audience, :id + attr_accessor :issued_at, :not_before, :expire_time + + def initialize + @id = SecureRandom.uuid + @issued_at = Time.now + # we give a few seconds for time shift + @not_before = issued_at - 5.seconds + # default 60 seconds should be more than enough for this authentication token + @expire_time = issued_at + 1.minute + @custom_payload = {} + end + + def [](key) + @custom_payload[key] + end + + def []=(key, value) + @custom_payload[key] = value + end + + def encoded + raise NotImplementedError + end + + def payload + @custom_payload.merge(default_payload) + end + + private + + def default_payload + { + jti: id, + aud: audience, + sub: subject, + iss: issuer, + iat: issued_at.to_i, + nbf: not_before.to_i, + exp: expire_time.to_i + }.compact + end + end +end diff --git a/lib/support/nginx/registry-ssl b/lib/support/nginx/registry-ssl new file mode 100644 index 00000000000..92511e26861 --- /dev/null +++ b/lib/support/nginx/registry-ssl @@ -0,0 +1,53 @@ +## Lines starting with two hashes (##) are comments with information. +## Lines starting with one hash (#) are configuration parameters that can be uncommented. +## +################################### +## configuration ## +################################### + +## Redirects all HTTP traffic to the HTTPS host +server { + listen *:80; + server_name registry.gitlab.example.com; + server_tokens off; ## Don't show the nginx version number, a security best practice + return 301 https://$http_host:$request_uri; + access_log /var/log/nginx/gitlab_registry_access.log gitlab_access; + error_log /var/log/nginx/gitlab_registry_error.log; +} + +server { + # If a different port is specified in https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/config/gitlab.yml.example#L182, + # it should be declared here as well + listen *:443 ssl http2; + server_name registry.gitlab.example.com; + server_tokens off; ## Don't show the nginx version number, a security best practice + + client_max_body_size 0; + chunked_transfer_encoding on; + + ## Strong SSL Security + ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/ + ssl on; + ssl_certificate /etc/gitlab/ssl/registry.gitlab.example.com.crt + ssl_certificate_key /etc/gitlab/ssl/registry.gitlab.example.com.key + + ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4'; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + ssl_session_cache builtin:1000 shared:SSL:10m; + ssl_session_timeout 5m; + + access_log /var/log/gitlab/nginx/gitlab_registry_access.log gitlab_access; + error_log /var/log/gitlab/nginx/gitlab_registry_error.log; + + location / { + proxy_set_header Host $http_host; # required for docker client's sake + proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 900; + + proxy_pass http://localhost:5000; + } + +} diff --git a/lib/tasks/auto_annotate_models.rake b/lib/tasks/auto_annotate_models.rake deleted file mode 100644 index 16bad4bd2bd..00000000000 --- a/lib/tasks/auto_annotate_models.rake +++ /dev/null @@ -1,44 +0,0 @@ -if Rails.env.development? - task :set_annotation_options do - # You can override any of these by setting an environment variable of the - # same name. - Annotate.set_defaults( - 'routes' => 'false', - 'position_in_routes' => 'before', - 'position_in_class' => 'before', - 'position_in_test' => 'before', - 'position_in_fixture' => 'before', - 'position_in_factory' => 'before', - 'position_in_serializer' => 'before', - 'show_foreign_keys' => 'true', - 'show_indexes' => 'false', - 'simple_indexes' => 'false', - 'model_dir' => 'app/models', - 'root_dir' => '', - 'include_version' => 'false', - 'require' => '', - 'exclude_tests' => 'true', - 'exclude_fixtures' => 'true', - 'exclude_factories' => 'true', - 'exclude_serializers' => 'true', - 'exclude_scaffolds' => 'true', - 'exclude_controllers' => 'true', - 'exclude_helpers' => 'true', - 'ignore_model_sub_dir' => 'false', - 'ignore_columns' => nil, - 'ignore_unknown_models' => 'false', - 'hide_limit_column_types' => 'integer,boolean', - 'skip_on_db_migrate' => 'false', - 'format_bare' => 'true', - 'format_rdoc' => 'false', - 'format_markdown' => 'false', - 'sort' => 'false', - 'force' => 'false', - 'trace' => 'false', - 'wrapper_open' => nil, - 'wrapper_close' => nil, - ) - end - - Annotate.load_tasks -end diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 402bb338f27..9ee72fde92f 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -14,6 +14,7 @@ namespace :gitlab do Rake::Task["gitlab:backup:builds:create"].invoke Rake::Task["gitlab:backup:artifacts:create"].invoke Rake::Task["gitlab:backup:lfs:create"].invoke + Rake::Task["gitlab:backup:registry:create"].invoke backup = Backup::Manager.new backup.pack @@ -39,14 +40,14 @@ namespace :gitlab do removed. MSG ask_to_continue - puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.yellow + puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.color(:yellow) sleep(5) end # Drop all tables Load the schema to ensure we don't have any newer tables # hanging out from a failed upgrade - $progress.puts 'Cleaning the database ... '.blue + $progress.puts 'Cleaning the database ... '.color(:blue) Rake::Task['gitlab:db:drop_tables'].invoke - $progress.puts 'done'.green + $progress.puts 'done'.color(:green) Rake::Task['gitlab:backup:db:restore'].invoke end Rake::Task['gitlab:backup:repo:restore'].invoke unless backup.skipped?('repositories') @@ -54,6 +55,7 @@ namespace :gitlab do Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds') Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts') Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs') + Rake::Task['gitlab:backup:registry:restore'].invoke unless backup.skipped?('registry') Rake::Task['gitlab:shell:setup'].invoke backup.cleanup @@ -61,115 +63,142 @@ namespace :gitlab do namespace :repo do task create: :environment do - $progress.puts "Dumping repositories ...".blue + $progress.puts "Dumping repositories ...".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("repositories") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Repository.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring repositories ...".blue + $progress.puts "Restoring repositories ...".color(:blue) Backup::Repository.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :db do task create: :environment do - $progress.puts "Dumping database ... ".blue + $progress.puts "Dumping database ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("db") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Database.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring database ... ".blue + $progress.puts "Restoring database ... ".color(:blue) Backup::Database.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :builds do task create: :environment do - $progress.puts "Dumping builds ... ".blue + $progress.puts "Dumping builds ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("builds") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Builds.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring builds ... ".blue + $progress.puts "Restoring builds ... ".color(:blue) Backup::Builds.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :uploads do task create: :environment do - $progress.puts "Dumping uploads ... ".blue + $progress.puts "Dumping uploads ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("uploads") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Uploads.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring uploads ... ".blue + $progress.puts "Restoring uploads ... ".color(:blue) Backup::Uploads.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :artifacts do task create: :environment do - $progress.puts "Dumping artifacts ... ".blue + $progress.puts "Dumping artifacts ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("artifacts") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Artifacts.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring artifacts ... ".blue + $progress.puts "Restoring artifacts ... ".color(:blue) Backup::Artifacts.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) end end namespace :lfs do task create: :environment do - $progress.puts "Dumping lfs objects ... ".blue + $progress.puts "Dumping lfs objects ... ".color(:blue) if ENV["SKIP"] && ENV["SKIP"].include?("lfs") - $progress.puts "[SKIPPED]".cyan + $progress.puts "[SKIPPED]".color(:cyan) else Backup::Lfs.new.dump - $progress.puts "done".green + $progress.puts "done".color(:green) end end task restore: :environment do - $progress.puts "Restoring lfs objects ... ".blue + $progress.puts "Restoring lfs objects ... ".color(:blue) Backup::Lfs.new.restore - $progress.puts "done".green + $progress.puts "done".color(:green) + end + end + + namespace :registry do + task create: :environment do + $progress.puts "Dumping container registry images ... ".color(:blue) + + if Gitlab.config.registry.enabled + if ENV["SKIP"] && ENV["SKIP"].include?("registry") + $progress.puts "[SKIPPED]".color(:cyan) + else + Backup::Registry.new.dump + $progress.puts "done".color(:green) + end + else + $progress.puts "[DISABLED]".color(:cyan) + end + end + + task restore: :environment do + $progress.puts "Restoring container registry images ... ".color(:blue) + if Gitlab.config.registry.enabled + Backup::Registry.new.restore + $progress.puts "done".color(:green) + else + $progress.puts "[DISABLED]".color(:cyan) + end end end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index effb8eb6001..12d6ac45fb6 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -50,14 +50,14 @@ namespace :gitlab do end if correct_options.all? - puts "yes".green + puts "yes".color(:green) else print "Trying to fix Git error automatically. ..." if auto_fix_git_config(options) - puts "Success".green + puts "Success".color(:green) else - puts "Failed".red + puts "Failed".color(:red) try_fixing_it( sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"") ) @@ -74,9 +74,9 @@ namespace :gitlab do database_config_file = Rails.root.join("config", "database.yml") if File.exists?(database_config_file) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Copy config/database.yml.<your db> to config/database.yml", "Check that the information in config/database.yml is correct" @@ -95,9 +95,9 @@ namespace :gitlab do gitlab_config_file = Rails.root.join("config", "gitlab.yml") if File.exists?(gitlab_config_file) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Copy config/gitlab.yml.example to config/gitlab.yml", "Update config/gitlab.yml to match your setup" @@ -114,14 +114,14 @@ namespace :gitlab do gitlab_config_file = Rails.root.join("config", "gitlab.yml") unless File.exists?(gitlab_config_file) - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) end # omniauth or ldap could have been deleted from the file unless Gitlab.config['git_host'] - puts "no".green + puts "no".color(:green) else - puts "yes".red + puts "yes".color(:red) try_fixing_it( "Backup your config/gitlab.yml", "Copy config/gitlab.yml.example to config/gitlab.yml", @@ -138,16 +138,16 @@ namespace :gitlab do print "Init script exists? ... " if omnibus_gitlab? - puts 'skipped (omnibus-gitlab has no init script)'.magenta + puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta) return end script_path = "/etc/init.d/gitlab" if File.exists?(script_path) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Install the init script" ) @@ -162,7 +162,7 @@ namespace :gitlab do print "Init script up-to-date? ... " if omnibus_gitlab? - puts 'skipped (omnibus-gitlab has no init script)'.magenta + puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta) return end @@ -170,7 +170,7 @@ namespace :gitlab do script_path = "/etc/init.d/gitlab" unless File.exists?(script_path) - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) return end @@ -178,9 +178,9 @@ namespace :gitlab do script_content = File.read(script_path) if recipe_content == script_content - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Redownload the init script" ) @@ -197,9 +197,9 @@ namespace :gitlab do migration_status, _ = Gitlab::Popen.popen(%W(bundle exec rake db:migrate:status)) unless migration_status =~ /down\s+\d{14}/ - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( sudo_gitlab("bundle exec rake db:migrate RAILS_ENV=production") ) @@ -210,13 +210,13 @@ namespace :gitlab do def check_orphaned_group_members print "Database contains orphaned GroupMembers? ... " if GroupMember.where("user_id not in (select id from users)").count > 0 - puts "yes".red + puts "yes".color(:red) try_fixing_it( "You can delete the orphaned records using something along the lines of:", sudo_gitlab("bundle exec rails runner -e production 'GroupMember.where(\"user_id NOT IN (SELECT id FROM users)\").delete_all'") ) else - puts "no".green + puts "no".color(:green) end end @@ -226,9 +226,9 @@ namespace :gitlab do log_path = Rails.root.join("log") if File.writable?(log_path) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo chown -R gitlab #{log_path}", "sudo chmod -R u+rwX #{log_path}" @@ -246,9 +246,9 @@ namespace :gitlab do tmp_path = Rails.root.join("tmp") if File.writable?(tmp_path) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo chown -R gitlab #{tmp_path}", "sudo chmod -R u+rwX #{tmp_path}" @@ -264,7 +264,7 @@ namespace :gitlab do print "Uploads directory setup correctly? ... " unless File.directory?(Rails.root.join('public/uploads')) - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo -u #{gitlab_user} mkdir #{Rails.root}/public/uploads" ) @@ -280,16 +280,16 @@ namespace :gitlab do if File.stat(upload_path).mode == 040700 unless Dir.exists?(upload_path_tmp) - puts 'skipped (no tmp uploads folder yet)'.magenta + puts 'skipped (no tmp uploads folder yet)'.color(:magenta) return end # If tmp upload dir has incorrect permissions, assume others do as well # Verify drwx------ permissions if File.stat(upload_path_tmp).mode == 040700 && File.owned?(upload_path_tmp) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo chown -R #{gitlab_user} #{upload_path}", "sudo find #{upload_path} -type f -exec chmod 0644 {} \\;", @@ -301,9 +301,9 @@ namespace :gitlab do fix_and_rerun end else - puts "no".red + puts "no".color(:red) try_fixing_it( - "sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0700 {} \\;" + "sudo chmod 700 #{upload_path}" ) for_more_information( see_installation_guide_section "GitLab" @@ -320,9 +320,9 @@ namespace :gitlab do redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/) if redis_version && (Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version)) - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Update your redis server to a version >= #{min_redis_version}" ) @@ -361,10 +361,10 @@ namespace :gitlab do repo_base_path = Gitlab.config.gitlab_shell.repos_path if File.exists?(repo_base_path) - puts "yes".green + puts "yes".color(:green) else - puts "no".red - puts "#{repo_base_path} is missing".red + puts "no".color(:red) + puts "#{repo_base_path} is missing".color(:red) try_fixing_it( "This should have been created when setting up GitLab Shell.", "Make sure it's set correctly in config/gitlab.yml", @@ -382,14 +382,14 @@ namespace :gitlab do repo_base_path = Gitlab.config.gitlab_shell.repos_path unless File.exists?(repo_base_path) - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) return end unless File.symlink?(repo_base_path) - puts "no".green + puts "no".color(:green) else - puts "yes".red + puts "yes".color(:red) try_fixing_it( "Make sure it's set to the real directory in config/gitlab.yml" ) @@ -402,14 +402,14 @@ namespace :gitlab do repo_base_path = Gitlab.config.gitlab_shell.repos_path unless File.exists?(repo_base_path) - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) return end if File.stat(repo_base_path).mode.to_s(8).ends_with?("2770") - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "sudo chmod -R ug+rwX,o-rwx #{repo_base_path}", "sudo chmod -R ug-s #{repo_base_path}", @@ -429,17 +429,17 @@ namespace :gitlab do repo_base_path = Gitlab.config.gitlab_shell.repos_path unless File.exists?(repo_base_path) - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) return end uid = uid_for(gitlab_shell_ssh_user) gid = gid_for(gitlab_shell_owner_group) if File.stat(repo_base_path).uid == uid && File.stat(repo_base_path).gid == gid - puts "yes".green + puts "yes".color(:green) else - puts "no".red - puts " User id for #{gitlab_shell_ssh_user}: #{uid}. Groupd id for #{gitlab_shell_owner_group}: #{gid}".blue + puts "no".color(:red) + puts " User id for #{gitlab_shell_ssh_user}: #{uid}. Groupd id for #{gitlab_shell_owner_group}: #{gid}".color(:blue) try_fixing_it( "sudo chown -R #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group} #{repo_base_path}" ) @@ -456,7 +456,7 @@ namespace :gitlab do gitlab_shell_hooks_path = Gitlab.config.gitlab_shell.hooks_path unless Project.count > 0 - puts "can't check, you have no projects".magenta + puts "can't check, you have no projects".color(:magenta) return end puts "" @@ -466,12 +466,12 @@ namespace :gitlab do project_hook_directory = File.join(project.repository.path_to_repo, "hooks") if project.empty_repo? - puts "repository is empty".magenta + puts "repository is empty".color(:magenta) elsif File.directory?(project_hook_directory) && File.directory?(gitlab_shell_hooks_path) && (File.realpath(project_hook_directory) == File.realpath(gitlab_shell_hooks_path)) - puts 'ok'.green + puts 'ok'.color(:green) else - puts "wrong or missing hooks".red + puts "wrong or missing hooks".color(:red) try_fixing_it( sudo_gitlab("#{File.join(gitlab_shell_path, 'bin/create-hooks')}"), 'Check the hooks_path in config/gitlab.yml', @@ -491,9 +491,9 @@ namespace :gitlab do check_cmd = File.expand_path('bin/check', gitlab_shell_repo_base) puts "Running #{check_cmd}" if system(check_cmd, chdir: gitlab_shell_repo_base) - puts 'gitlab-shell self-check successful'.green + puts 'gitlab-shell self-check successful'.color(:green) else - puts 'gitlab-shell self-check failed'.red + puts 'gitlab-shell self-check failed'.color(:red) try_fixing_it( 'Make sure GitLab is running;', 'Check the gitlab-shell configuration file:', @@ -507,7 +507,7 @@ namespace :gitlab do print "projects have namespace: ... " unless Project.count > 0 - puts "can't check, you have no projects".magenta + puts "can't check, you have no projects".color(:magenta) return end puts "" @@ -516,9 +516,9 @@ namespace :gitlab do print sanitized_message(project) if project.namespace - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Migrate global projects" ) @@ -576,9 +576,9 @@ namespace :gitlab do print "Running? ... " if sidekiq_process_count > 0 - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( sudo_gitlab("RAILS_ENV=production bin/background_jobs start") ) @@ -596,9 +596,9 @@ namespace :gitlab do print 'Number of Sidekiq processes ... ' if process_count == 1 - puts '1'.green + puts '1'.color(:green) else - puts "#{process_count}".red + puts "#{process_count}".color(:red) try_fixing_it( 'sudo service gitlab stop', "sudo pkill -u #{gitlab_user} -f sidekiq", @@ -646,16 +646,16 @@ namespace :gitlab do print "Init.d configured correctly? ... " if omnibus_gitlab? - puts 'skipped (omnibus-gitlab has no init script)'.magenta + puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta) return end path = "/etc/default/gitlab" if File.exist?(path) && File.read(path).include?("mail_room_enabled=true") - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Enable mail_room in the init.d configuration." ) @@ -672,9 +672,9 @@ namespace :gitlab do path = Rails.root.join("Procfile") if File.exist?(path) && File.read(path) =~ /^mail_room:/ - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Enable mail_room in your Procfile." ) @@ -691,14 +691,14 @@ namespace :gitlab do path = "/etc/default/gitlab" unless File.exist?(path) && File.read(path).include?("mail_room_enabled=true") - puts "can't check because of previous errors".magenta + puts "can't check because of previous errors".color(:magenta) return end if mail_room_running? - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( sudo_gitlab("RAILS_ENV=production bin/mail_room start") ) @@ -729,9 +729,9 @@ namespace :gitlab do end if connected - puts "yes".green + puts "yes".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Check that the information in config/gitlab.yml is correct" ) @@ -799,7 +799,7 @@ namespace :gitlab do namespace :user do desc "GitLab | Check the integrity of a specific user's repositories" task :check_repos, [:username] => :environment do |t, args| - username = args[:username] || prompt("Check repository integrity for which username? ".blue) + username = args[:username] || prompt("Check repository integrity for which username? ".color(:blue)) user = User.find_by(username: username) if user repo_dirs = user.authorized_projects.map do |p| @@ -811,7 +811,7 @@ namespace :gitlab do repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) } else - puts "\nUser '#{username}' not found".red + puts "\nUser '#{username}' not found".color(:red) end end end @@ -820,13 +820,13 @@ namespace :gitlab do ########################## def fix_and_rerun - puts " Please #{"fix the error above"} and rerun the checks.".red + puts " Please #{"fix the error above"} and rerun the checks.".color(:red) end def for_more_information(*sources) sources = sources.shift if sources.first.is_a?(Array) - puts " For more information see:".blue + puts " For more information see:".color(:blue) sources.each do |source| puts " #{source}" end @@ -834,7 +834,7 @@ namespace :gitlab do def finished_checking(component) puts "" - puts "Checking #{component.yellow} ... #{"Finished".green}" + puts "Checking #{component.color(:yellow)} ... #{"Finished".color(:green)}" puts "" end @@ -855,14 +855,14 @@ namespace :gitlab do end def start_checking(component) - puts "Checking #{component.yellow} ..." + puts "Checking #{component.color(:yellow)} ..." puts "" end def try_fixing_it(*steps) steps = steps.shift if steps.first.is_a?(Array) - puts " Try fixing it:".blue + puts " Try fixing it:".color(:blue) steps.each do |step| puts " #{step}" end @@ -874,9 +874,9 @@ namespace :gitlab do print "GitLab Shell version >= #{required_version} ? ... " if current_version.valid? && required_version <= current_version - puts "OK (#{current_version})".green + puts "OK (#{current_version})".color(:green) else - puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".red + puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".color(:red) end end @@ -887,9 +887,9 @@ namespace :gitlab do print "Ruby version >= #{required_version} ? ... " if current_version.valid? && required_version <= current_version - puts "yes (#{current_version})".green + puts "yes (#{current_version})".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Update your ruby to a version >= #{required_version} from #{current_version}" ) @@ -905,9 +905,9 @@ namespace :gitlab do print "Git version >= #{required_version} ? ... " if current_version.valid? && required_version <= current_version - puts "yes (#{current_version})".green + puts "yes (#{current_version})".color(:green) else - puts "no".red + puts "no".color(:red) try_fixing_it( "Update your git to a version >= #{required_version} from #{current_version}" ) @@ -925,9 +925,9 @@ namespace :gitlab do def sanitized_message(project) if should_sanitize? - "#{project.namespace_id.to_s.yellow}/#{project.id.to_s.yellow} ... " + "#{project.namespace_id.to_s.color(:yellow)}/#{project.id.to_s.color(:yellow)} ... " else - "#{project.name_with_namespace.yellow} ... " + "#{project.name_with_namespace.color(:yellow)} ... " end end @@ -940,7 +940,7 @@ namespace :gitlab do end def check_repo_integrity(repo_dir) - puts "\nChecking repo at #{repo_dir.yellow}" + puts "\nChecking repo at #{repo_dir.color(:yellow)}" git_fsck(repo_dir) check_config_lock(repo_dir) @@ -948,25 +948,25 @@ namespace :gitlab do end def git_fsck(repo_dir) - puts "Running `git fsck`".yellow + puts "Running `git fsck`".color(:yellow) system(*%W(#{Gitlab.config.git.bin_path} fsck), chdir: repo_dir) end def check_config_lock(repo_dir) config_exists = File.exist?(File.join(repo_dir,'config.lock')) - config_output = config_exists ? 'yes'.red : 'no'.green - puts "'config.lock' file exists?".yellow + " ... #{config_output}" + config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green) + puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}" end def check_ref_locks(repo_dir) lock_files = Dir.glob(File.join(repo_dir,'refs/heads/*.lock')) if lock_files.present? - puts "Ref lock files exist:".red + puts "Ref lock files exist:".color(:red) lock_files.each do |lock_file| puts " #{lock_file}" end else - puts "No ref lock files exist".green + puts "No ref lock files exist".color(:green) end end end diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index 9f5852ac613..ab0028d6603 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -10,7 +10,7 @@ namespace :gitlab do git_base_path = Gitlab.config.gitlab_shell.repos_path all_dirs = Dir.glob(git_base_path + '/*') - puts git_base_path.yellow + puts git_base_path.color(:yellow) puts "Looking for directories to remove... " all_dirs.reject! do |dir| @@ -29,17 +29,17 @@ namespace :gitlab do if remove_flag if FileUtils.rm_rf dir_path - puts "Removed...#{dir_path}".red + puts "Removed...#{dir_path}".color(:red) else - puts "Cannot remove #{dir_path}".red + puts "Cannot remove #{dir_path}".color(:red) end else - puts "Can be removed: #{dir_path}".red + puts "Can be removed: #{dir_path}".color(:red) end end unless remove_flag - puts "To cleanup this directories run this command with REMOVE=true".yellow + puts "To cleanup this directories run this command with REMOVE=true".color(:yellow) end end @@ -75,19 +75,19 @@ namespace :gitlab do next unless user.ldap_user? print "#{user.name} (#{user.ldap_identity.extern_uid}) ..." if Gitlab::LDAP::Access.allowed?(user) - puts " [OK]".green + puts " [OK]".color(:green) else if block_flag user.block! unless user.blocked? - puts " [BLOCKED]".red + puts " [BLOCKED]".color(:red) else - puts " [NOT IN LDAP]".yellow + puts " [NOT IN LDAP]".color(:yellow) end end end unless block_flag - puts "To block these users run this command with BLOCK=true".yellow + puts "To block these users run this command with BLOCK=true".color(:yellow) end end end diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index 1c706dc11b3..7230b9485be 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -3,22 +3,22 @@ namespace :gitlab do desc 'GitLab | Manually insert schema migration version' task :mark_migration_complete, [:version] => :environment do |_, args| unless args[:version] - puts "Must specify a migration version as an argument".red + puts "Must specify a migration version as an argument".color(:red) exit 1 end version = args[:version].to_i if version == 0 - puts "Version '#{args[:version]}' must be a non-zero integer".red + puts "Version '#{args[:version]}' must be a non-zero integer".color(:red) exit 1 end sql = "INSERT INTO schema_migrations (version) VALUES (#{version})" begin ActiveRecord::Base.connection.execute(sql) - puts "Successfully marked '#{version}' as complete".green + puts "Successfully marked '#{version}' as complete".color(:green) rescue ActiveRecord::RecordNotUnique - puts "Migration version '#{version}' is already marked complete".yellow + puts "Migration version '#{version}' is already marked complete".color(:yellow) end end @@ -29,10 +29,22 @@ namespace :gitlab do tables.delete 'schema_migrations' # Truncate schema_migrations to ensure migrations re-run connection.execute('TRUNCATE schema_migrations') + # Drop tables with cascade to avoid dependent table errors # PG: http://www.postgresql.org/docs/current/static/ddl-depend.html # MySQL: http://dev.mysql.com/doc/refman/5.7/en/drop-table.html - tables.each { |t| connection.execute("DROP TABLE #{t} CASCADE") } + # Add `IF EXISTS` because cascade could have already deleted a table. + tables.each { |t| connection.execute("DROP TABLE IF EXISTS #{connection.quote_table_name(t)} CASCADE") } + end + + desc 'Configures the database by running migrate, or by loading the schema and seeding if needed' + task configure: :environment do + if ActiveRecord::Base.connection.tables.any? + Rake::Task['db:migrate'].invoke + else + Rake::Task['db:schema:load'].invoke + Rake::Task['db:seed_fu'].invoke + end end end end diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake index 65ee430d550..f9834a4dae8 100644 --- a/lib/tasks/gitlab/git.rake +++ b/lib/tasks/gitlab/git.rake @@ -5,7 +5,7 @@ namespace :gitlab do task repack: :environment do failures = perform_git_cmd(%W(git repack -a --quiet), "Repacking repo") if failures.empty? - puts "Done".green + puts "Done".color(:green) else output_failures(failures) end @@ -15,7 +15,7 @@ namespace :gitlab do task gc: :environment do failures = perform_git_cmd(%W(git gc --auto --quiet), "Garbage Collecting") if failures.empty? - puts "Done".green + puts "Done".color(:green) else output_failures(failures) end @@ -25,7 +25,7 @@ namespace :gitlab do task prune: :environment do failures = perform_git_cmd(%W(git prune), "Git Prune") if failures.empty? - puts "Done".green + puts "Done".color(:green) else output_failures(failures) end @@ -47,7 +47,7 @@ namespace :gitlab do end def output_failures(failures) - puts "The following repositories reported errors:".red + puts "The following repositories reported errors:".color(:red) failures.each { |f| puts "- #{f}" } end diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index 1c04f47f08f..4753f00c26a 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -23,7 +23,7 @@ namespace :gitlab do group_name, name = File.split(path) group_name = nil if group_name == '.' - puts "Processing #{repo_path}".yellow + puts "Processing #{repo_path}".color(:yellow) if path.end_with?('.wiki') puts " * Skipping wiki repo" @@ -51,9 +51,9 @@ namespace :gitlab do group.path = group_name group.owner = user if group.save - puts " * Created Group #{group.name} (#{group.id})".green + puts " * Created Group #{group.name} (#{group.id})".color(:green) else - puts " * Failed trying to create group #{group.name}".red + puts " * Failed trying to create group #{group.name}".color(:red) end end # set project group @@ -63,17 +63,17 @@ namespace :gitlab do project = Projects::CreateService.new(user, project_params).execute if project.persisted? - puts " * Created #{project.name} (#{repo_path})".green + puts " * Created #{project.name} (#{repo_path})".color(:green) project.update_repository_size project.update_commit_count else - puts " * Failed trying to create #{project.name} (#{repo_path})".red - puts " Errors: #{project.errors.messages}".red + puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red) + puts " Errors: #{project.errors.messages}".color(:red) end end end - puts "Done!".green + puts "Done!".color(:green) end end end diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake index d6883a563ee..352b566df24 100644 --- a/lib/tasks/gitlab/info.rake +++ b/lib/tasks/gitlab/info.rake @@ -15,15 +15,15 @@ namespace :gitlab do rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s) puts "" - puts "System information".yellow - puts "System:\t\t#{os_name || "unknown".red}" + puts "System information".color(:yellow) + puts "System:\t\t#{os_name || "unknown".color(:red)}" puts "Current User:\t#{run(%W(whoami))}" - puts "Using RVM:\t#{rvm_version.present? ? "yes".green : "no"}" + puts "Using RVM:\t#{rvm_version.present? ? "yes".color(:green) : "no"}" puts "RVM Version:\t#{rvm_version}" if rvm_version.present? - puts "Ruby Version:\t#{ruby_version || "unknown".red}" - puts "Gem Version:\t#{gem_version || "unknown".red}" - puts "Bundler Version:#{bunder_version || "unknown".red}" - puts "Rake Version:\t#{rake_version || "unknown".red}" + puts "Ruby Version:\t#{ruby_version || "unknown".color(:red)}" + puts "Gem Version:\t#{gem_version || "unknown".color(:red)}" + puts "Bundler Version:#{bunder_version || "unknown".color(:red)}" + puts "Rake Version:\t#{rake_version || "unknown".color(:red)}" puts "Sidekiq Version:#{Sidekiq::VERSION}" @@ -39,7 +39,7 @@ namespace :gitlab do omniauth_providers.map! { |provider| provider['name'] } puts "" - puts "GitLab information".yellow + puts "GitLab information".color(:yellow) puts "Version:\t#{Gitlab::VERSION}" puts "Revision:\t#{Gitlab::REVISION}" puts "Directory:\t#{Rails.root}" @@ -47,9 +47,9 @@ namespace :gitlab do puts "URL:\t\t#{Gitlab.config.gitlab.url}" puts "HTTP Clone URL:\t#{http_clone_url}" puts "SSH Clone URL:\t#{ssh_clone_url}" - puts "Using LDAP:\t#{Gitlab.config.ldap.enabled ? "yes".green : "no"}" - puts "Using Omniauth:\t#{Gitlab.config.omniauth.enabled ? "yes".green : "no"}" - puts "Omniauth Providers: #{omniauth_providers.map(&:magenta).join(', ')}" if Gitlab.config.omniauth.enabled + puts "Using LDAP:\t#{Gitlab.config.ldap.enabled ? "yes".color(:green) : "no"}" + puts "Using Omniauth:\t#{Gitlab.config.omniauth.enabled ? "yes".color(:green) : "no"}" + puts "Omniauth Providers: #{omniauth_providers.join(', ')}" if Gitlab.config.omniauth.enabled @@ -60,8 +60,8 @@ namespace :gitlab do end puts "" - puts "GitLab Shell".yellow - puts "Version:\t#{gitlab_shell_version || "unknown".red}" + puts "GitLab Shell".color(:yellow) + puts "Version:\t#{gitlab_shell_version || "unknown".color(:red)}" puts "Repositories:\t#{Gitlab.config.gitlab_shell.repos_path}" puts "Hooks:\t\t#{Gitlab.config.gitlab_shell.hooks_path}" puts "Git:\t\t#{Gitlab.config.git.bin_path}" diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake index 48baecfd2a2..05fcb8e3da5 100644 --- a/lib/tasks/gitlab/setup.rake +++ b/lib/tasks/gitlab/setup.rake @@ -19,7 +19,7 @@ namespace :gitlab do Rake::Task["setup_postgresql"].invoke Rake::Task["db:seed_fu"].invoke rescue Gitlab::TaskAbortedByUserError - puts "Quitting...".red + puts "Quitting...".color(:red) exit 1 end end diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index dd61632e557..b1648a4602a 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -118,12 +118,12 @@ namespace :gitlab do puts "" unless $?.success? - puts "Failed to add keys...".red + puts "Failed to add keys...".color(:red) exit 1 end rescue Gitlab::TaskAbortedByUserError - puts "Quitting...".red + puts "Quitting...".color(:red) exit 1 end diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index d33b5b31e18..d0c019044b7 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -2,7 +2,7 @@ module Gitlab class TaskAbortedByUserError < StandardError; end end -String.disable_colorization = true unless STDOUT.isatty +require 'rainbow/ext/string' # Prevent StateMachine warnings from outputting during a cron task StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON'] @@ -14,7 +14,7 @@ namespace :gitlab do # Returns "yes" the user chose to continue # Raises Gitlab::TaskAbortedByUserError if the user chose *not* to continue def ask_to_continue - answer = prompt("Do you want to continue (yes/no)? ".blue, %w{yes no}) + answer = prompt("Do you want to continue (yes/no)? ".color(:blue), %w{yes no}) raise Gitlab::TaskAbortedByUserError unless answer == "yes" end @@ -98,10 +98,10 @@ namespace :gitlab do gitlab_user = Gitlab.config.gitlab.user current_user = run(%W(whoami)).chomp unless current_user == gitlab_user - puts " Warning ".colorize(:black).on_yellow - puts " You are running as user #{current_user.magenta}, we hope you know what you are doing." + puts " Warning ".color(:black).background(:yellow) + puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing." puts " Things may work\/fail for the wrong reasons." - puts " For correct results you should run this as user #{gitlab_user.magenta}." + puts " For correct results you should run this as user #{gitlab_user.color(:magenta)}." puts "" end @warned_user_not_gitlab = true diff --git a/lib/tasks/gitlab/two_factor.rake b/lib/tasks/gitlab/two_factor.rake index 9196677a017..fc0ccc726ed 100644 --- a/lib/tasks/gitlab/two_factor.rake +++ b/lib/tasks/gitlab/two_factor.rake @@ -6,17 +6,17 @@ namespace :gitlab do count = scope.count if count > 0 - puts "This will disable 2FA for #{count.to_s.red} users..." + puts "This will disable 2FA for #{count.to_s.color(:red)} users..." begin ask_to_continue scope.find_each(&:disable_two_factor!) - puts "Successfully disabled 2FA for #{count} users.".green + puts "Successfully disabled 2FA for #{count} users.".color(:green) rescue Gitlab::TaskAbortedByUserError - puts "Quitting...".red + puts "Quitting...".color(:red) end else - puts "There are currently no users with 2FA enabled.".yellow + puts "There are currently no users with 2FA enabled.".color(:yellow) end end end diff --git a/lib/tasks/gitlab/update_commit_count.rake b/lib/tasks/gitlab/update_commit_count.rake index 9b636f12d9f..3bd10b0208b 100644 --- a/lib/tasks/gitlab/update_commit_count.rake +++ b/lib/tasks/gitlab/update_commit_count.rake @@ -6,15 +6,15 @@ namespace :gitlab do ask_to_continue unless ENV['force'] == 'yes' projects.find_each(batch_size: 100) do |project| - print "#{project.name_with_namespace.yellow} ... " + print "#{project.name_with_namespace.color(:yellow)} ... " unless project.repo_exists? - puts "skipping, because the repo is empty".magenta + puts "skipping, because the repo is empty".color(:magenta) next end project.update_commit_count - puts project.commit_count.to_s.green + puts project.commit_count.to_s.color(:green) end end end diff --git a/lib/tasks/gitlab/update_gitignore.rake b/lib/tasks/gitlab/update_gitignore.rake new file mode 100644 index 00000000000..4fd48cccb1d --- /dev/null +++ b/lib/tasks/gitlab/update_gitignore.rake @@ -0,0 +1,46 @@ +namespace :gitlab do + desc "GitLab | Update gitignore" + task :update_gitignore do + unless clone_gitignores + puts "Cloning the gitignores failed".color(:red) + return + end + + remove_unneeded_files(gitignore_directory) + remove_unneeded_files(global_directory) + + puts "Done".color(:green) + end + + def clone_gitignores + FileUtils.rm_rf(gitignore_directory) if Dir.exist?(gitignore_directory) + FileUtils.cd vendor_directory + + system('git clone --depth=1 --branch=master https://github.com/github/gitignore.git') + end + + # Retain only certain files: + # - The LICENSE, because we have to + # - The sub dir global + # - The gitignores themself + # - Dir.entires returns also the entries '.' and '..' + def remove_unneeded_files(path) + Dir.foreach(path) do |file| + FileUtils.rm_rf(File.join(path, file)) unless file =~ /(\.{1,2}|LICENSE|Global|\.gitignore)\z/ + end + end + + private + + def vendor_directory + Rails.root.join('vendor') + end + + def gitignore_directory + File.join(vendor_directory, 'gitignore') + end + + def global_directory + File.join(gitignore_directory, 'Global') + end +end diff --git a/lib/tasks/gitlab/web_hook.rake b/lib/tasks/gitlab/web_hook.rake index cc0f668474e..f467cc0ee29 100644 --- a/lib/tasks/gitlab/web_hook.rake +++ b/lib/tasks/gitlab/web_hook.rake @@ -12,9 +12,9 @@ namespace :gitlab do print "- #{project.name} ... " web_hook = project.hooks.new(url: web_hook_url) if web_hook.save - puts "added".green + puts "added".color(:green) else - print "failed".red + print "failed".color(:red) puts " [#{web_hook.errors.full_messages.to_sentence}]" end end @@ -57,7 +57,7 @@ namespace :gitlab do if namespace Project.in_namespace(namespace.id) else - puts "Namespace not found: #{namespace_path}".red + puts "Namespace not found: #{namespace_path}".color(:red) exit 2 end end diff --git a/lib/tasks/migrate/migrate_iids.rake b/lib/tasks/migrate/migrate_iids.rake index d258c6fd08d..4f2486157b7 100644 --- a/lib/tasks/migrate/migrate_iids.rake +++ b/lib/tasks/migrate/migrate_iids.rake @@ -1,6 +1,6 @@ desc "GitLab | Build internal ids for issues and merge requests" task migrate_iids: :environment do - puts 'Issues'.yellow + puts 'Issues'.color(:yellow) Issue.where(iid: nil).find_each(batch_size: 100) do |issue| begin issue.set_iid @@ -15,7 +15,7 @@ task migrate_iids: :environment do end puts 'done' - puts 'Merge Requests'.yellow + puts 'Merge Requests'.color(:yellow) MergeRequest.where(iid: nil).find_each(batch_size: 100) do |mr| begin mr.set_iid @@ -30,7 +30,7 @@ task migrate_iids: :environment do end puts 'done' - puts 'Milestones'.yellow + puts 'Milestones'.color(:yellow) Milestone.where(iid: nil).find_each(batch_size: 100) do |m| begin m.set_iid diff --git a/lib/tasks/rubocop.rake b/lib/tasks/rubocop.rake index ddfaf5d51f2..78ffccc9d06 100644 --- a/lib/tasks/rubocop.rake +++ b/lib/tasks/rubocop.rake @@ -1,4 +1,5 @@ unless Rails.env.production? require 'rubocop/rake_task' + RuboCop::RakeTask.new end diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake index 01d23b89bb7..da255f5464b 100644 --- a/lib/tasks/spinach.rake +++ b/lib/tasks/spinach.rake @@ -52,7 +52,7 @@ def run_spinach_tests(tags) tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp) puts '' - puts "Spinach tests for #{tags}: Retrying tests... #{tests}".red + puts "Spinach tests for #{tags}: Retrying tests... #{tests}".color(:red) puts '' sleep(3) success = run_spinach_command(tests) diff --git a/public/robots.txt b/public/robots.txt index 4f616c7f4c1..334f4c03533 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -65,3 +65,4 @@ Disallow: /*/*/deploy_keys Disallow: /*/*/hooks Disallow: /*/*/services Disallow: /*/*/protected_branches +Disallow: /*/*/uploads/ diff --git a/scripts/merge-reports b/scripts/merge-reports new file mode 100755 index 00000000000..f7b574001ac --- /dev/null +++ b/scripts/merge-reports @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby + +require 'json' +require 'yaml' + +main_report_file = ARGV.shift +unless main_report_file + puts 'usage: merge_reports <main-report> [extra reports...]' + exit 1 +end + +puts "Loading #{main_report_file}..." +main_report = JSON.parse(File.read(main_report_file)) +new_report = main_report.dup + +ARGV.each do |report_file| + report = JSON.parse(File.read(report_file)) + + # Remove existing values + updates = report.delete_if do |key, value| + main_report[key] && main_report[key] == value + end + new_report.merge!(updates) + + puts "Merged #{report_file} adding #{updates.size} results." +end + +File.write(main_report_file, JSON.pretty_generate(new_report)) +puts "Saved #{main_report_file}." diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 247383aa46c..d6fb1a34e8c 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -1,12 +1,16 @@ #!/bin/bash retry() { - for i in $(seq 1 3); do + if eval "$@"; then + return 0 + fi + + for i in 2 1; do + sleep 3s + echo "Retrying $i..." if eval "$@"; then return 0 fi - sleep 3s - echo "Retrying..." done return 1 } diff --git a/shared/registry/.gitkeep b/shared/registry/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/shared/registry/.gitkeep diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb index 462afb24f08..6fad7e2b9e7 100644 --- a/spec/config/mail_room_spec.rb +++ b/spec/config/mail_room_spec.rb @@ -43,7 +43,7 @@ describe "mail_room.yml" do redis_config_file = Rails.root.join('config', 'resque.yml') redis_url = - if File.exists?(redis_config_file) + if File.exist?(redis_config_file) YAML.load_file(redis_config_file)[Rails.env] else "redis://localhost:6379" diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb index 2ba0d489197..4cb8b8da150 100644 --- a/spec/controllers/admin/projects_controller_spec.rb +++ b/spec/controllers/admin/projects_controller_spec.rb @@ -17,7 +17,7 @@ describe Admin::ProjectsController do it 'does not retrieve the project' do get :index, visibility_levels: [Gitlab::VisibilityLevel::INTERNAL] - expect(response.body).to_not match(project.name) + expect(response.body).not_to match(project.name) end end end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index ce2a62ae1fd..6caf37ddc2c 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -114,6 +114,82 @@ describe Admin::UsersController do end end + describe 'POST update' do + context 'when the password has changed' do + def update_password(user, password, password_confirmation = nil) + params = { + id: user.to_param, + user: { + password: password, + password_confirmation: password_confirmation || password + } + } + + post :update, params + end + + context 'when the new password is valid' do + it 'redirects to the user' do + update_password(user, 'AValidPassword1') + + expect(response).to redirect_to(admin_user_path(user)) + end + + it 'updates the password' do + update_password(user, 'AValidPassword1') + + expect { user.reload }.to change { user.encrypted_password } + end + + it 'sets the new password to expire immediately' do + update_password(user, 'AValidPassword1') + + expect { user.reload }.to change { user.password_expires_at }.to(a_value <= Time.now) + end + end + + context 'when the new password is invalid' do + it 'shows the edit page again' do + update_password(user, 'invalid') + + expect(response).to render_template(:edit) + end + + it 'returns the error message' do + update_password(user, 'invalid') + + expect(assigns[:user].errors).to contain_exactly(a_string_matching(/too short/)) + end + + it 'does not update the password' do + update_password(user, 'invalid') + + expect { user.reload }.not_to change { user.encrypted_password } + end + end + + context 'when the new password does not match the password confirmation' do + it 'shows the edit page again' do + update_password(user, 'AValidPassword1', 'AValidPassword2') + + expect(response).to render_template(:edit) + end + + it 'returns the error message' do + update_password(user, 'AValidPassword1', 'AValidPassword2') + + expect(assigns[:user].errors).to contain_exactly(a_string_matching(/doesn't match/)) + end + + it 'does not update the password' do + update_password(user, 'AValidPassword1', 'AValidPassword2') + + expect { user.reload }.not_to change { user.encrypted_password } + end + end + end + end + describe "POST impersonate" do context "when the user is blocked" do before do diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 465531b2b36..cd98fecd0c7 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -31,9 +31,9 @@ describe GroupsController do let(:issue_2) { create(:issue, project: project) } before do - create_list(:upvote_note, 3, project: project, noteable: issue_2) - create_list(:upvote_note, 2, project: project, noteable: issue_1) - create_list(:downvote_note, 2, project: project, noteable: issue_2) + create_list(:award_emoji, 3, awardable: issue_2) + create_list(:award_emoji, 2, awardable: issue_1) + create_list(:award_emoji, 2, :downvote, awardable: issue_2,) sign_in(user) end @@ -56,9 +56,9 @@ describe GroupsController do let(:merge_request_2) { create(:merge_request, :simple, source_project: project) } before do - create_list(:upvote_note, 3, project: project, noteable: merge_request_2) - create_list(:upvote_note, 2, project: project, noteable: merge_request_1) - create_list(:downvote_note, 2, project: project, noteable: merge_request_2) + create_list(:award_emoji, 3, awardable: merge_request_2) + create_list(:award_emoji, 2, awardable: merge_request_1) + create_list(:award_emoji, 2, :downvote, awardable: merge_request_2) sign_in(user) end diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb new file mode 100644 index 00000000000..0d8a68bb51a --- /dev/null +++ b/spec/controllers/health_check_controller_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper' + +describe HealthCheckController do + let(:token) { current_application_settings.health_check_access_token } + let(:json_response) { JSON.parse(response.body) } + let(:xml_response) { Hash.from_xml(response.body)['hash'] } + + describe 'GET #index' do + context 'when services are up but NO access token' do + it 'returns a not found page' do + get :index + expect(response).to be_not_found + end + end + + context 'when services are up and an access token is provided' do + it 'supports passing the token in the header' do + request.headers['TOKEN'] = token + get :index + expect(response).to be_success + expect(response.content_type).to eq 'text/plain' + end + + it 'supports successful plaintest response' do + get :index, token: token + expect(response).to be_success + expect(response.content_type).to eq 'text/plain' + end + + it 'supports successful json response' do + get :index, token: token, format: :json + expect(response).to be_success + expect(response.content_type).to eq 'application/json' + expect(json_response['healthy']).to be true + end + + it 'supports successful xml response' do + get :index, token: token, format: :xml + expect(response).to be_success + expect(response.content_type).to eq 'application/xml' + expect(xml_response['healthy']).to be true + end + + it 'supports successful responses for specific checks' do + get :index, token: token, checks: 'email', format: :json + expect(response).to be_success + expect(response.content_type).to eq 'application/json' + expect(json_response['healthy']).to be true + end + end + + context 'when a service is down but NO access token' do + it 'returns a not found page' do + get :index + expect(response).to be_not_found + end + end + + context 'when a service is down and an access token is provided' do + before do + allow(HealthCheck::Utils).to receive(:process_checks).with('standard').and_return('The server is on fire') + allow(HealthCheck::Utils).to receive(:process_checks).with('email').and_return('Email is on fire') + end + + it 'supports passing the token in the header' do + request.headers['TOKEN'] = token + get :index + expect(response.status).to eq(500) + expect(response.content_type).to eq 'text/plain' + expect(response.body).to include('The server is on fire') + end + + it 'supports failure plaintest response' do + get :index, token: token + expect(response.status).to eq(500) + expect(response.content_type).to eq 'text/plain' + expect(response.body).to include('The server is on fire') + end + + it 'supports failure json response' do + get :index, token: token, format: :json + expect(response.status).to eq(500) + expect(response.content_type).to eq 'application/json' + expect(json_response['healthy']).to be false + expect(json_response['message']).to include('The server is on fire') + end + + it 'supports failure xml response' do + get :index, token: token, format: :xml + expect(response.status).to eq(500) + expect(response.content_type).to eq 'application/xml' + expect(xml_response['healthy']).to be false + expect(xml_response['message']).to include('The server is on fire') + end + + it 'supports failure responses for specific checks' do + get :index, token: token, checks: 'email', format: :json + expect(response.status).to eq(500) + expect(response.content_type).to eq 'application/json' + expect(json_response['healthy']).to be false + expect(json_response['message']).to include('Email is on fire') + end + end + end +end diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 81c03c9059b..07bf8d2d1c3 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require_relative 'import_spec_helper' describe Import::BitbucketController do include ImportSpecHelper diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb index 27b11267d2a..5f0f6dea821 100644 --- a/spec/controllers/import/fogbugz_controller_spec.rb +++ b/spec/controllers/import/fogbugz_controller_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require_relative 'import_spec_helper' describe Import::FogbugzController do include ImportSpecHelper diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index bcc713dce2a..c55a3c28208 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require_relative 'import_spec_helper' describe Import::GithubController do include ImportSpecHelper diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb index 198d006af76..e8cf6aa7767 100644 --- a/spec/controllers/import/gitlab_controller_spec.rb +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require_relative 'import_spec_helper' describe Import::GitlabController do include ImportSpecHelper diff --git a/spec/controllers/import/gitorious_controller_spec.rb b/spec/controllers/import/gitorious_controller_spec.rb index 7cb1b85a46d..4ae2b78e11c 100644 --- a/spec/controllers/import/gitorious_controller_spec.rb +++ b/spec/controllers/import/gitorious_controller_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require_relative 'import_spec_helper' describe Import::GitoriousController do include ImportSpecHelper diff --git a/spec/controllers/import/google_code_controller_spec.rb b/spec/controllers/import/google_code_controller_spec.rb index 66088139a69..4241db6e771 100644 --- a/spec/controllers/import/google_code_controller_spec.rb +++ b/spec/controllers/import/google_code_controller_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -require_relative 'import_spec_helper' describe Import::GoogleCodeController do include ImportSpecHelper diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb new file mode 100644 index 00000000000..af378304893 --- /dev/null +++ b/spec/controllers/oauth/applications_controller_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Oauth::ApplicationsController do + let(:user) { create(:user) } + + context 'project members' do + before do + sign_in(user) + end + + describe 'GET #index' do + it 'shows list of applications' do + get :index + + expect(response.status).to eq(200) + end + + it 'redirects back to profile page if OAuth applications are disabled' do + settings = double(user_oauth_applications?: false) + allow_any_instance_of(Gitlab::CurrentSettings).to receive(:current_application_settings).and_return(settings) + + get :index + + expect(response.status).to eq(302) + expect(response).to redirect_to(profile_path) + end + end + end +end diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb index 4fb1473c2d2..d08d0018b35 100644 --- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb +++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb @@ -8,21 +8,21 @@ describe Profiles::TwoFactorAuthsController do allow(subject).to receive(:current_user).and_return(user) end - describe 'GET new' do + describe 'GET show' do let(:user) { create(:user) } it 'generates otp_secret for user' do expect(User).to receive(:generate_otp_secret).with(32).and_return('secret').once - get :new - get :new # Second hit shouldn't re-generate it + get :show + get :show # Second hit shouldn't re-generate it end it 'assigns qr_code' do code = double('qr code') expect(subject).to receive(:build_qr_code).and_return(code) - get :new + get :show expect(assigns[:qr_code]).to eq code end end @@ -40,7 +40,7 @@ describe Profiles::TwoFactorAuthsController do expect(user).to receive(:validate_and_consume_otp!).with(pin).and_return(true) end - it 'sets two_factor_enabled' do + it 'enables 2fa for the user' do go user.reload @@ -79,9 +79,9 @@ describe Profiles::TwoFactorAuthsController do expect(assigns[:qr_code]).to eq code end - it 'renders new' do + it 'renders show' do go - expect(response).to render_template(:new) + expect(response).to render_template(:show) end end end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 8ad73472117..c4b4a888b4e 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -122,27 +122,23 @@ describe Projects::BranchesController do let(:branch) { "feature" } it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } end context "valid branch name with unencoded slashes" do let(:branch) { "improve/awesome" } it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } end context "valid branch name with encoded slashes" do let(:branch) { "improve%2Fawesome" } it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } end context "invalid branch name, valid ref" do let(:branch) { "no-branch" } it { expect(response.status).to eq(404) } - it { expect(subject).to render_template('destroy') } end end end diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index 788a609ee40..4018dac95a2 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -19,7 +19,7 @@ describe Projects::CompareController do to: ref_to) expect(response).to be_success - expect(assigns(:diffs).first).to_not be_nil + expect(assigns(:diffs).first).not_to be_nil expect(assigns(:commits).length).to be >= 1 end @@ -32,7 +32,7 @@ describe Projects::CompareController do w: 1) expect(response).to be_success - expect(assigns(:diffs).first).to_not be_nil + expect(assigns(:diffs).first).not_to be_nil expect(assigns(:commits).length).to be >= 1 # without whitespace option, there are more than 2 diff_splits diff_splits = assigns(:diffs).first.diff.split("\n") diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb index 40bd83af861..fbe8758dda7 100644 --- a/spec/controllers/projects/group_links_controller_spec.rb +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -28,7 +28,7 @@ describe Projects::GroupLinksController do expect(group.shared_projects).to include project end - it 'redirects to project group links page'do + it 'redirects to project group links page' do expect(response).to redirect_to( namespace_project_group_links_path(project.namespace, project) ) @@ -43,7 +43,7 @@ describe Projects::GroupLinksController do end it 'does not share project with that group' do - expect(group.shared_projects).to_not include project + expect(group.shared_projects).not_to include project end end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 2b2ad3b9412..78be7e3dc35 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -56,7 +56,7 @@ describe Projects::IssuesController do move_issue expect(response).to have_http_status :found - expect(another_project.issues).to_not be_empty + expect(another_project.issues).not_to be_empty end end @@ -250,4 +250,20 @@ describe Projects::IssuesController do end end end + + describe 'POST #toggle_award_emoji' do + before do + sign_in(user) + project.team << [user, :developer] + end + + it "toggles the award emoji" do + expect do + post(:toggle_award_emoji, namespace_id: project.namespace.path, + project_id: project.path, id: issue.iid, name: "thumbsup") + end.to change { issue.award_emoji.count }.by(1) + + expect(response.status).to eq(200) + end + end end diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb new file mode 100644 index 00000000000..ab1dd34ed57 --- /dev/null +++ b/spec/controllers/projects/labels_controller_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Projects::LabelsController do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.team << [user, :master] + sign_in(user) + end + + describe 'GET #index' do + def create_label(attributes) + create(:label, attributes.merge(project: project)) + end + + before do + 15.times { |i| create_label(priority: (i % 3) + 1, title: "label #{15 - i}") } + 5.times { |i| create_label(title: "label #{100 - i}") } + + + get :index, namespace_id: project.namespace.to_param, project_id: project.to_param + end + + context '@prioritized_labels' do + let(:prioritized_labels) { assigns(:prioritized_labels) } + + it 'contains only prioritized labels' do + expect(prioritized_labels).to all(have_attributes(priority: a_value > 0)) + end + + it 'is sorted by priority, then label title' do + priorities_and_titles = prioritized_labels.pluck(:priority, :title) + + expect(priorities_and_titles.sort).to eq(priorities_and_titles) + end + end + + context '@labels' do + let(:labels) { assigns(:labels) } + + it 'contains only unprioritized labels' do + expect(labels).to all(have_attributes(priority: nil)) + end + + it 'is sorted by label title' do + titles = labels.pluck(:title) + + expect(titles.sort).to eq(titles) + end + end + end +end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c0a1f45195f..4b408c03703 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -63,7 +63,7 @@ describe Projects::MergeRequestsController do id: merge_request.iid, format: format) - expect(response.body).to eq((merge_request.send(:"to_#{format}")).to_s) + expect(response.body).to eq(merge_request.send(:"to_#{format}").to_s) end it "should not escape Html" do @@ -84,17 +84,14 @@ describe Projects::MergeRequestsController do end describe "as diff" do - include_examples "export merge as", :diff - let(:format) { :diff } - - it "should really only be a git diff" do + it "triggers workhorse to serve the request" do get(:show, namespace_id: project.namespace.to_param, project_id: project.to_param, id: merge_request.iid, - format: format) + format: :diff) - expect(response.body).to start_with("diff --git") + expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:") end end @@ -185,6 +182,92 @@ describe Projects::MergeRequestsController do end end + describe 'POST #merge' do + let(:base_params) do + { + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + format: 'raw' + } + end + + context 'when the user does not have access' do + before do + project.team.truncate + project.team << [user, :reporter] + post :merge, base_params + end + + it 'returns not found' do + expect(response).to be_not_found + end + end + + context 'when the merge request is not mergeable' do + before do + merge_request.update_attributes(title: "WIP: #{merge_request.title}") + + post :merge, base_params + end + + it 'returns :failed' do + expect(assigns(:status)).to eq(:failed) + end + end + + context 'when the sha parameter does not match the source SHA' do + before { post :merge, base_params.merge(sha: 'foo') } + + it 'returns :sha_mismatch' do + expect(assigns(:status)).to eq(:sha_mismatch) + end + end + + context 'when the sha parameter matches the source SHA' do + def merge_with_sha + post :merge, base_params.merge(sha: merge_request.source_sha) + end + + it 'returns :success' do + merge_with_sha + + expect(assigns(:status)).to eq(:success) + end + + it 'starts the merge immediately' do + expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, anything) + + merge_with_sha + end + + context 'when merge_when_build_succeeds is passed' do + def merge_when_build_succeeds + post :merge, base_params.merge(sha: merge_request.source_sha, merge_when_build_succeeds: '1') + end + + before do + create(:ci_empty_pipeline, project: project, sha: merge_request.source_sha, ref: merge_request.source_branch) + end + + it 'returns :merge_when_build_succeeds' do + merge_when_build_succeeds + + expect(assigns(:status)).to eq(:merge_when_build_succeeds) + end + + it 'sets the MR to merge when the build succeeds' do + service = double(:merge_when_build_succeeds_service) + + expect(MergeRequests::MergeWhenBuildSucceedsService).to receive(:new).with(project, anything, anything).and_return(service) + expect(service).to receive(:execute).with(merge_request) + + merge_when_build_succeeds + end + end + end + end + describe "DELETE #destroy" do it "denies access to users unless they're admin or project owner" do delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb new file mode 100644 index 00000000000..00bc38b6071 --- /dev/null +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -0,0 +1,36 @@ +require('spec_helper') + +describe Projects::NotesController do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:issue) { create(:issue, project: project) } + let(:note) { create(:note, noteable: issue, project: project) } + + describe 'POST #toggle_award_emoji' do + before do + sign_in(user) + project.team << [user, :developer] + end + + it "toggles the award emoji" do + expect do + post(:toggle_award_emoji, namespace_id: project.namespace.path, + project_id: project.path, id: note.id, name: "thumbsup") + end.to change { note.award_emoji.count }.by(1) + + expect(response.status).to eq(200) + end + + it "removes the already awarded emoji" do + post(:toggle_award_emoji, namespace_id: project.namespace.path, + project_id: project.path, id: note.id, name: "thumbsup") + + expect do + post(:toggle_award_emoji, namespace_id: project.namespace.path, + project_id: project.path, id: note.id, name: "thumbsup") + end.to change { AwardEmoji.count }.by(-1) + + expect(response.status).to eq(200) + end + end +end diff --git a/spec/controllers/projects/notification_settings_controller_spec.rb b/spec/controllers/projects/notification_settings_controller_spec.rb index 4908b545648..c5d17d97ec9 100644 --- a/spec/controllers/projects/notification_settings_controller_spec.rb +++ b/spec/controllers/projects/notification_settings_controller_spec.rb @@ -34,5 +34,19 @@ describe Projects::NotificationSettingsController do expect(response.status).to eq 200 end end + + context 'not authorized' do + let(:private_project) { create(:project, :private) } + before { sign_in(user) } + + it 'returns 404' do + put :update, + namespace_id: private_project.namespace.to_param, + project_id: private_project.to_param, + notification_setting: { level: :participating } + + expect(response.status).to eq(404) + end + end end end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index ed64e7cf9af..750fbecdd07 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -38,7 +38,7 @@ describe Projects::ProjectMembersController do include_context 'import applied' it 'does not import team members' do - expect(project.team_members).to_not include member + expect(project.team_members).not_to include member end it 'responds with not found' do diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index 1caa476d37d..33c35161da3 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -17,6 +17,7 @@ describe Projects::RawController do expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') expect(response.header['Content-Disposition']). to eq("inline") + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:") end end @@ -31,6 +32,7 @@ describe Projects::RawController do expect(response.status).to eq(200) expect(response.header['Content-Type']).to eq('image/jpeg') + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:") end end @@ -42,7 +44,7 @@ describe Projects::RawController do before do public_project.lfs_objects << lfs_object allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true) - allow(controller).to receive(:send_file) { controller.render nothing: true } + allow(controller).to receive(:send_file) { controller.head :ok } end it 'serves the file' do diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb index 0ddbec9eac2..aad62cf20e3 100644 --- a/spec/controllers/projects/repositories_controller_spec.rb +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -20,10 +20,11 @@ describe Projects::RepositoriesController do project.team << [user, :developer] sign_in(user) end - it "uses Gitlab::Workhorse" do - expect(Gitlab::Workhorse).to receive(:send_git_archive).with(project, "master", "zip") + it "uses Gitlab::Workhorse" do get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") end context "when the service raises an error" do diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 069cd917e5a..fba545560c7 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -8,6 +8,40 @@ describe ProjectsController do let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } describe "GET show" do + context "user not project member" do + before { sign_in(user) } + + context "user does not have access to project" do + let(:private_project) { create(:project, :private) } + + it "does not initialize notification setting" do + get :show, namespace_id: private_project.namespace.path, id: private_project.path + expect(assigns(:notification_setting)).to be_nil + end + end + + context "user has access to project" do + context "and does not have notification setting" do + it "initializes notification as disabled" do + get :show, namespace_id: public_project.namespace.path, id: public_project.path + expect(assigns(:notification_setting).level).to eq("global") + end + end + + context "and has notification setting" do + before do + setting = user.notification_settings_for(public_project) + setting.level = :watch + setting.save + end + + it "shows current notification setting" do + get :show, namespace_id: public_project.namespace.path, id: public_project.path + expect(assigns(:notification_setting).level).to eq("watch") + end + end + end + end context "rendering default project view" do render_views @@ -81,6 +115,17 @@ describe ProjectsController do expect(public_project_with_dot_atom).not_to be_valid end end + + context 'when the project is pending deletions' do + it 'renders a 404 error' do + project = create(:project, pending_delete: true) + sign_in(user) + + get :show, namespace_id: project.namespace.path, id: project.path + + expect(response.status).to eq 404 + end + end end describe "#update" do diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb new file mode 100644 index 00000000000..209fa37d97d --- /dev/null +++ b/spec/controllers/registrations_controller_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe RegistrationsController do + describe '#create' do + around(:each) do |example| + perform_enqueued_jobs do + example.run + end + end + + let(:user_params) { { user: { name: "new_user", username: "new_username", email: "new@user.com", password: "Any_password" } } } + + context 'when sending email confirmation' do + before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) } + + it 'logs user in directly' do + post(:create, user_params) + expect(ActionMailer::Base.deliveries.last).to be_nil + expect(subject.current_user).not_to be_nil + end + end + + context 'when not sending email confirmation' do + before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) } + + it 'does not authenticate user and sends confirmation email' do + post(:create, user_params) + expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) + expect(subject.current_user).to be_nil + end + end + end +end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 83cc8ec6d26..4e9bfb0c69b 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -12,7 +12,7 @@ describe SessionsController do post(:create, user: { login: 'invalid', password: 'invalid' }) expect(response) - .to set_flash.now[:alert].to /Invalid login or password/ + .to set_flash.now[:alert].to /Invalid Login or password/ end end @@ -25,16 +25,42 @@ describe SessionsController do expect(response).to set_flash.to /Signed in successfully/ expect(subject.current_user). to eq user end + + it "creates an audit log record" do + expect { post(:create, user: { login: user.username, password: user.password }) }.to change { SecurityEvent.count }.by(1) + expect(SecurityEvent.last.details[:with]).to eq("standard") + end end end - context 'when using two-factor authentication' do + context 'when using two-factor authentication via OTP' do let(:user) { create(:user, :two_factor) } def authenticate_2fa(user_params) post(:create, { user: user_params }, { otp_user_id: user.id }) end + context 'remember_me field' do + it 'sets a remember_user_token cookie when enabled' do + allow(controller).to receive(:find_user).and_return(user) + expect(controller). + to receive(:remember_me).with(user).and_call_original + + authenticate_2fa(remember_me: '1', otp_attempt: user.current_otp) + + expect(response.cookies['remember_user_token']).to be_present + end + + it 'does nothing when disabled' do + allow(controller).to receive(:find_user).and_return(user) + expect(controller).not_to receive(:remember_me) + + authenticate_2fa(remember_me: '0', otp_attempt: user.current_otp) + + expect(response.cookies['remember_user_token']).to be_nil + end + end + ## # See #14900 issue # @@ -47,7 +73,7 @@ describe SessionsController do authenticate_2fa(login: another_user.username, otp_attempt: another_user.current_otp) - expect(subject.current_user).to_not eq another_user + expect(subject.current_user).not_to eq another_user end end @@ -56,7 +82,7 @@ describe SessionsController do authenticate_2fa(login: another_user.username, otp_attempt: 'invalid') - expect(subject.current_user).to_not eq another_user + expect(subject.current_user).not_to eq another_user end end @@ -73,7 +99,7 @@ describe SessionsController do before { authenticate_2fa(otp_attempt: 'invalid') } it 'does not authenticate' do - expect(subject.current_user).to_not eq user + expect(subject.current_user).not_to eq user end it 'warns about invalid OTP code' do @@ -96,6 +122,25 @@ describe SessionsController do end end end + + it "creates an audit log record" do + expect { authenticate_2fa(login: user.username, otp_attempt: user.current_otp) }.to change { SecurityEvent.count }.by(1) + expect(SecurityEvent.last.details[:with]).to eq("two-factor") + end + end + + context 'when using two-factor authentication via U2F device' do + let(:user) { create(:user, :two_factor) } + + def authenticate_2fa_u2f(user_params) + post(:create, { user: user_params }, { otp_user_id: user.id }) + end + + it "creates an audit log record" do + allow(U2fRegistration).to receive(:authenticate).and_return(true) + expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { SecurityEvent.count }.by(1) + expect(SecurityEvent.last.details[:with]).to eq("two-factor-via-u2f-device") + end end end end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 8045c8b940d..c61ec174665 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -112,4 +112,26 @@ describe UsersController do expect(response).to render_template('calendar_activities') end end + + describe 'GET #snippets' do + before do + sign_in(user) + end + + context 'format html' do + it 'renders snippets page' do + get :snippets, username: user.username + expect(response.status).to eq(200) + expect(response).to render_template('show') + end + end + + context 'format json' do + it 'response with snippets json data' do + get :snippets, username: user.username, format: :json + expect(response.status).to eq(200) + expect(JSON.parse(response.body)).to have_key('html') + end + end + end end diff --git a/spec/factories/abuse_reports.rb b/spec/factories/abuse_reports.rb index d0e8c778518..8f6422a7825 100644 --- a/spec/factories/abuse_reports.rb +++ b/spec/factories/abuse_reports.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: abuse_reports -# -# id :integer not null, primary key -# reporter_id :integer -# user_id :integer -# message :text -# created_at :datetime -# updated_at :datetime -# - FactoryGirl.define do factory :abuse_report do reporter factory: :user diff --git a/spec/factories/award_emoji.rb b/spec/factories/award_emoji.rb new file mode 100644 index 00000000000..4b858df52c9 --- /dev/null +++ b/spec/factories/award_emoji.rb @@ -0,0 +1,12 @@ +FactoryGirl.define do + factory :award_emoji do + name "thumbsup" + user + awardable factory: :issue + + trait :upvote + trait :downvote do + name "thumbsdown" + end + end +end diff --git a/spec/factories/broadcast_messages.rb b/spec/factories/broadcast_messages.rb index c80e7366551..efe9803b1a7 100644 --- a/spec/factories/broadcast_messages.rb +++ b/spec/factories/broadcast_messages.rb @@ -1,17 +1,3 @@ -# == Schema Information -# -# Table name: broadcast_messages -# -# id :integer not null, primary key -# message :text not null -# starts_at :datetime -# ends_at :datetime -# created_at :datetime -# updated_at :datetime -# color :string(255) -# font :string(255) -# - FactoryGirl.define do factory :broadcast_message do message "MyText" diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index cd49e559b7d..fe05a0cfc00 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -16,7 +16,7 @@ FactoryGirl.define do } end - commit factory: :ci_commit + pipeline factory: :ci_pipeline trait :success do status 'success' @@ -43,7 +43,7 @@ FactoryGirl.define do end after(:build) do |build, evaluator| - build.project = build.commit.project + build.project = build.pipeline.project end factory :ci_not_started_build do diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb index 645cd7ae766..a039bef6f3c 100644 --- a/spec/factories/ci/commits.rb +++ b/spec/factories/ci/commits.rb @@ -17,30 +17,30 @@ # FactoryGirl.define do - factory :ci_empty_commit, class: Ci::Commit do + factory :ci_empty_pipeline, class: Ci::Pipeline do sha '97de212e80737a608d939f648d959671fb0a0142' project factory: :empty_project - factory :ci_commit_without_jobs do + factory :ci_pipeline_without_jobs do after(:build) do |commit| allow(commit).to receive(:ci_yaml_file) { YAML.dump({}) } end end - factory :ci_commit_with_one_job do + factory :ci_pipeline_with_one_job do after(:build) do |commit| allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" } }) } end end - factory :ci_commit_with_two_jobs do + factory :ci_pipeline_with_two_job do after(:build) do |commit| allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } }) } end end - factory :ci_commit do + factory :ci_pipeline do after(:build) do |commit| allow(commit).to receive(:ci_yaml_file) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) } end diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index b7c2b32cb13..1e5c479616c 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -3,12 +3,12 @@ FactoryGirl.define do name 'default' status 'success' description 'commit status' - commit factory: :ci_commit_with_one_job + pipeline factory: :ci_pipeline_with_one_job started_at 'Tue, 26 Jan 2016 08:21:42 +0100' finished_at 'Tue, 26 Jan 2016 08:23:42 +0100' after(:build) do |build, evaluator| - build.project = build.commit.project + build.project = build.pipeline.project end factory :generic_commit_status, class: GenericCommitStatus do diff --git a/spec/factories/forked_project_links.rb b/spec/factories/forked_project_links.rb index 19a54946fe0..b16c1272e68 100644 --- a/spec/factories/forked_project_links.rb +++ b/spec/factories/forked_project_links.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: forked_project_links -# -# id :integer not null, primary key -# forked_to_project_id :integer not null -# forked_from_project_id :integer not null -# created_at :datetime -# updated_at :datetime -# - FactoryGirl.define do factory :forked_project_link do association :forked_to_project, factory: :project diff --git a/spec/factories/label_links.rb b/spec/factories/label_links.rb index 2939d4307c5..3580174e873 100644 --- a/spec/factories/label_links.rb +++ b/spec/factories/label_links.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: label_links -# -# id :integer not null, primary key -# label_id :integer -# target_id :integer -# target_type :string(255) -# created_at :datetime -# updated_at :datetime -# - FactoryGirl.define do factory :label_link do label diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb index ea2be8928d5..eb489099854 100644 --- a/spec/factories/labels.rb +++ b/spec/factories/labels.rb @@ -1,16 +1,3 @@ -# == Schema Information -# -# Table name: labels -# -# id :integer not null, primary key -# title :string(255) -# color :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# template :boolean default(FALSE) -# - FactoryGirl.define do factory :label do sequence(:title) { |n| "label#{n}" } diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb index 327858ce435..a81645acd2b 100644 --- a/spec/factories/lfs_objects.rb +++ b/spec/factories/lfs_objects.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: lfs_objects -# -# id :integer not null, primary key -# oid :string(255) not null -# size :integer not null -# created_at :datetime -# updated_at :datetime -# file :string(255) -# - include ActionDispatch::TestProcess FactoryGirl.define do diff --git a/spec/factories/lfs_objects_projects.rb b/spec/factories/lfs_objects_projects.rb index 50b45843c99..1ed0355c8e4 100644 --- a/spec/factories/lfs_objects_projects.rb +++ b/spec/factories/lfs_objects_projects.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: lfs_objects_projects -# -# id :integer not null, primary key -# lfs_object_id :integer not null -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# - FactoryGirl.define do factory :lfs_objects_project do lfs_object diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index e281e2f227b..c6a08d78b78 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -1,32 +1,3 @@ -# == Schema Information -# -# Table name: merge_requests -# -# id :integer not null, primary key -# target_branch :string(255) not null -# source_branch :string(255) not null -# source_project_id :integer not null -# author_id :integer -# assignee_id :integer -# title :string(255) -# created_at :datetime -# updated_at :datetime -# milestone_id :integer -# state :string(255) -# merge_status :string(255) -# target_project_id :integer not null -# iid :integer -# description :text -# position :integer default(0) -# locked_at :datetime -# updated_by_id :integer -# merge_error :string(255) -# merge_params :text -# merge_when_build_succeeds :boolean default(FALSE), not null -# merge_user_id :integer -# merge_commit_sha :string -# - FactoryGirl.define do factory :merge_request do title diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index e5dcb159014..696cf276e57 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -1,24 +1,3 @@ -# == Schema Information -# -# Table name: notes -# -# id :integer not null, primary key -# note :text -# noteable_type :string(255) -# author_id :integer -# created_at :datetime -# updated_at :datetime -# project_id :integer -# attachment :string(255) -# line_code :string(255) -# commit_id :string(255) -# noteable_id :integer -# system :boolean default(FALSE), not null -# st_diff :text -# updated_by_id :integer -# is_award :boolean default(FALSE), not null -# - require_relative '../support/repo_helpers' include ActionDispatch::TestProcess @@ -28,51 +7,43 @@ FactoryGirl.define do project note "Note" author + on_issue factory :note_on_commit, traits: [:on_commit] - factory :note_on_commit_diff, traits: [:on_commit, :on_diff] + factory :note_on_commit_diff, traits: [:on_commit, :on_diff], class: LegacyDiffNote factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note] factory :note_on_merge_request, traits: [:on_merge_request] - factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff] + factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff], class: LegacyDiffNote factory :note_on_project_snippet, traits: [:on_project_snippet] factory :system_note, traits: [:system] - factory :downvote_note, traits: [:award, :downvote] - factory :upvote_note, traits: [:award, :upvote] trait :on_commit do - project + noteable nil + noteable_id nil + noteable_type 'Commit' commit_id RepoHelpers.sample_commit.id - noteable_type "Commit" end trait :on_diff do line_code "0_184_184" end - trait :on_merge_request do - project - noteable_id 1 - noteable_type "MergeRequest" + trait :on_issue do + noteable { create(:issue, project: project) } end - trait :on_issue do - noteable_id 1 - noteable_type "Issue" + trait :on_merge_request do + noteable { create(:merge_request, source_project: project) } end trait :on_project_snippet do - noteable_id 1 - noteable_type "Snippet" + noteable { create(:snippet, project: project) } end trait :system do system true end - trait :award do - is_award true - end - trait :downvote do note "thumbsdown" end diff --git a/spec/factories/oauth_access_tokens.rb b/spec/factories/oauth_access_tokens.rb index 7700b15d538..ccf02d0719b 100644 --- a/spec/factories/oauth_access_tokens.rb +++ b/spec/factories/oauth_access_tokens.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: oauth_access_tokens -# -# id :integer not null, primary key -# resource_owner_id :integer -# application_id :integer -# token :string not null -# refresh_token :string -# expires_in :integer -# revoked_at :datetime -# created_at :datetime not null -# scopes :string -# - FactoryGirl.define do factory :oauth_access_token do resource_owner diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index ecd0e44dd62..da8d97c9f82 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -1,42 +1,3 @@ -# == Schema Information -# -# Table name: projects -# -# id :integer not null, primary key -# name :string(255) -# path :string(255) -# description :text -# created_at :datetime -# updated_at :datetime -# creator_id :integer -# issues_enabled :boolean default(TRUE), not null -# merge_requests_enabled :boolean default(TRUE), not null -# wiki_enabled :boolean default(TRUE), not null -# namespace_id :integer -# issues_tracker :string(255) default("gitlab"), not null -# issues_tracker_id :string(255) -# snippets_enabled :boolean default(TRUE), not null -# last_activity_at :datetime -# import_url :string(255) -# visibility_level :integer default(0), not null -# archived :boolean default(FALSE), not null -# avatar :string(255) -# import_status :string(255) -# repository_size :float default(0.0) -# star_count :integer default(0), not null -# import_type :string(255) -# import_source :string(255) -# commit_count :integer default(0) -# import_error :text -# ci_id :integer -# builds_enabled :boolean default(TRUE), not null -# shared_runners_enabled :boolean default(TRUE), not null -# runners_token :string -# build_coverage_regex :string -# build_allow_git_fetch :boolean default(TRUE), not null -# build_timeout :integer default(3600), not null -# - FactoryGirl.define do # Project without repository # @@ -60,6 +21,12 @@ FactoryGirl.define do trait :private do visibility_level Gitlab::VisibilityLevel::PRIVATE end + + trait :empty_repo do + after(:create) do |project| + project.create_repository + end + end end # Project with empty repository @@ -67,9 +34,7 @@ FactoryGirl.define do # This is a case when you just created a project # but not pushed any code there yet factory :project_empty_repo, parent: :empty_project do - after :create do |project| - project.create_repository - end + empty_repo end # Project with test repository diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb index 7f331c37256..74497dc82c0 100644 --- a/spec/factories/releases.rb +++ b/spec/factories/releases.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: releases -# -# id :integer not null, primary key -# tag :string(255) -# description :text -# project_id :integer -# created_at :datetime -# updated_at :datetime -# - FactoryGirl.define do factory :release do tag "v1.1.0" diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index 7ae06c27840..f426e27afed 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -1,21 +1,3 @@ -# == Schema Information -# -# Table name: todos -# -# id :integer not null, primary key -# user_id :integer not null -# project_id :integer not null -# target_id :integer -# target_type :string not null -# author_id :integer -# action :integer not null -# state :string not null -# created_at :datetime -# updated_at :datetime -# note_id :integer -# commit_id :string -# - FactoryGirl.define do factory :todo do project @@ -36,5 +18,9 @@ FactoryGirl.define do commit_id RepoHelpers.sample_commit.id target_type "Commit" end + + trait :build_failed do + action { Todo::BUILD_FAILED } + end end end diff --git a/spec/factories/u2f_registrations.rb b/spec/factories/u2f_registrations.rb new file mode 100644 index 00000000000..df92b079581 --- /dev/null +++ b/spec/factories/u2f_registrations.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :u2f_registration do + certificate { FFaker::BaconIpsum.characters(728) } + key_handle { FFaker::BaconIpsum.characters(86) } + public_key { FFaker::BaconIpsum.characters(88) } + counter 0 + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index a9b2148bd2a..c6f7869516e 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -15,14 +15,26 @@ FactoryGirl.define do end trait :two_factor do + two_factor_via_otp + end + + trait :two_factor_via_otp do before(:create) do |user| - user.two_factor_enabled = true + user.otp_required_for_login = true user.otp_secret = User.generate_otp_secret(32) user.otp_grace_period_started_at = Time.now user.generate_otp_backup_codes! end end + trait :two_factor_via_u2f do + transient { registrations_count 5 } + + after(:create) do |user, evaluator| + create_list(:u2f_registration, evaluator.registrations_count, user: user) + end + end + factory :omniauth_user do transient do extern_uid '123456' diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb index 938ccf2306b..efa6cbe5bb1 100644 --- a/spec/factories/wiki_pages.rb +++ b/spec/factories/wiki_pages.rb @@ -2,7 +2,7 @@ require 'ostruct' FactoryGirl.define do factory :wiki_page do - page = OpenStruct.new(url_path: 'some-name') + page { OpenStruct.new(url_path: 'some-name') } association :wiki, factory: :project_wiki, strategy: :build initialize_with { new(wiki, page, true) } end diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb index 62de081661d..675d9bd18b7 100644 --- a/spec/factories_spec.rb +++ b/spec/factories_spec.rb @@ -5,8 +5,8 @@ describe 'factories' do describe "#{factory.name} factory" do let(:entity) { build(factory.name) } - it 'does not raise error when created 'do - expect { entity }.to_not raise_error + it 'does not raise error when created' do + expect { entity }.not_to raise_error end it 'should be valid', if: factory.build_class < ActiveRecord::Base do diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index 2e9851fb442..a6198389f04 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -6,19 +6,20 @@ describe 'Admin Builds' do end describe 'GET /admin/builds' do - let(:commit) { create(:ci_commit) } + let(:pipeline) { create(:ci_pipeline) } context 'All tab' do context 'when have builds' do it 'shows all builds' do - create(:ci_build, commit: commit, status: :pending) - create(:ci_build, commit: commit, status: :running) - create(:ci_build, commit: commit, status: :success) - create(:ci_build, commit: commit, status: :failed) + create(:ci_build, pipeline: pipeline, status: :pending) + create(:ci_build, pipeline: pipeline, status: :running) + create(:ci_build, pipeline: pipeline, status: :success) + create(:ci_build, pipeline: pipeline, status: :failed) visit admin_builds_path expect(page).to have_selector('.nav-links li.active', text: 'All') + expect(page).to have_selector('.row-content-block', text: 'All builds') expect(page.all('.build-link').size).to eq(4) expect(page).to have_link 'Cancel all' end @@ -38,9 +39,9 @@ describe 'Admin Builds' do context 'Running tab' do context 'when have running builds' do it 'shows running builds' do - build1 = create(:ci_build, commit: commit, status: :pending) - build2 = create(:ci_build, commit: commit, status: :success) - build3 = create(:ci_build, commit: commit, status: :failed) + build1 = create(:ci_build, pipeline: pipeline, status: :pending) + build2 = create(:ci_build, pipeline: pipeline, status: :success) + build3 = create(:ci_build, pipeline: pipeline, status: :failed) visit admin_builds_path(scope: :running) @@ -54,7 +55,7 @@ describe 'Admin Builds' do context 'when have no builds running' do it 'shows a message' do - create(:ci_build, commit: commit, status: :success) + create(:ci_build, pipeline: pipeline, status: :success) visit admin_builds_path(scope: :running) @@ -68,9 +69,9 @@ describe 'Admin Builds' do context 'Finished tab' do context 'when have finished builds' do it 'shows finished builds' do - build1 = create(:ci_build, commit: commit, status: :pending) - build2 = create(:ci_build, commit: commit, status: :running) - build3 = create(:ci_build, commit: commit, status: :success) + build1 = create(:ci_build, pipeline: pipeline, status: :pending) + build2 = create(:ci_build, pipeline: pipeline, status: :running) + build3 = create(:ci_build, pipeline: pipeline, status: :success) visit admin_builds_path(scope: :finished) @@ -84,7 +85,7 @@ describe 'Admin Builds' do context 'when have no builds finished' do it 'shows a message' do - create(:ci_build, commit: commit, status: :running) + create(:ci_build, pipeline: pipeline, status: :running) visit admin_builds_path(scope: :finished) diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb new file mode 100644 index 00000000000..dec2dedf2b5 --- /dev/null +++ b/spec/features/admin/admin_health_check_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +feature "Admin Health Check", feature: true do + include WaitForAjax + + before do + login_as :admin + end + + describe '#show' do + before do + visit admin_health_check_path + end + + it { page.has_text? 'Health Check' } + it { page.has_text? 'Health information can be retrieved' } + + it 'has a health check access token' do + token = current_application_settings.health_check_access_token + expect(page).to have_content("Access token is #{token}") + expect(page).to have_selector('#health-check-token', text: token) + end + + describe 'reload access token', js: true do + it 'changes the access token' do + orig_token = current_application_settings.health_check_access_token + click_button 'Reset health check access token' + wait_for_ajax + expect(find('#health-check-token').text).not_to eq orig_token + end + end + end + + context 'when services are up' do + before do + visit admin_health_check_path + end + + it 'shows healthy status' do + expect(page).to have_content('Current Status: Healthy') + end + end + + context 'when a service is down' do + before do + allow(HealthCheck::Utils).to receive(:process_checks).and_return('The server is on fire') + visit admin_health_check_path + end + + it 'shows unhealthy status' do + expect(page).to have_content('Current Status: Unhealthy') + expect(page).to have_content('The server is on fire') + end + end +end diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index 26d03944b8a..9499cd4e025 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -8,8 +8,8 @@ describe "Admin Runners" do describe "Runners page" do before do runner = FactoryGirl.create(:ci_runner) - commit = FactoryGirl.create(:ci_commit) - FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) + pipeline = FactoryGirl.create(:ci_pipeline) + FactoryGirl.create(:ci_build, pipeline: pipeline, runner_id: runner.id) visit admin_runners_path end @@ -79,7 +79,7 @@ describe "Admin Runners" do end it 'changes registration token' do - expect(page_token).to_not eq token + expect(page_token).not_to eq token end end end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 4570e409128..1cb709c1de3 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -19,7 +19,7 @@ describe "Admin::Users", feature: true do describe 'Two-factor Authentication filters' do it 'counts users who have enabled 2FA' do - create(:user, two_factor_enabled: true) + create(:user, :two_factor) visit admin_users_path @@ -29,7 +29,7 @@ describe "Admin::Users", feature: true do end it 'filters by users who have enabled 2FA' do - user = create(:user, two_factor_enabled: true) + user = create(:user, :two_factor) visit admin_users_path click_link '2FA Enabled' @@ -38,7 +38,7 @@ describe "Admin::Users", feature: true do end it 'counts users who have not enabled 2FA' do - create(:user, two_factor_enabled: false) + create(:user) visit admin_users_path @@ -48,7 +48,7 @@ describe "Admin::Users", feature: true do end it 'filters by users who have not enabled 2FA' do - user = create(:user, two_factor_enabled: false) + user = create(:user) visit admin_users_path click_link '2FA Disabled' @@ -144,22 +144,22 @@ describe "Admin::Users", feature: true do before { click_link 'Impersonate' } it 'logs in as the user when impersonate is clicked' do - page.within '.sidebar-user .username' do - expect(page).to have_content(another_user.username) + page.within '.sidebar-wrapper' do + expect(page.find('.sidebar-user')['data-user']).to eql(another_user.username) end end it 'sees impersonation log out icon' do icon = first('.fa.fa-user-secret') - expect(icon).to_not eql nil + expect(icon).not_to eql nil end it 'can log out of impersonated user back to original user' do find(:css, 'li.impersonation a').click - page.within '.sidebar-user .username' do - expect(page).to have_content(@user.username) + page.within '.sidebar-wrapper' do + expect(page.find('.sidebar-user')['data-user']).to eql(@user.username) end end @@ -173,7 +173,7 @@ describe "Admin::Users", feature: true do describe 'Two-factor Authentication status' do it 'shows when enabled' do - @user.update_attribute(:two_factor_enabled, true) + @user.update_attribute(:otp_required_for_login, true) visit admin_user_path(@user) @@ -210,6 +210,8 @@ describe "Admin::Users", feature: true do before do fill_in "user_name", with: "Big Bang" fill_in "user_email", with: "bigbang@mail.com" + fill_in "user_password", with: "AValidPassword1" + fill_in "user_password_confirmation", with: "AValidPassword1" check "user_admin" click_button "Save changes" end @@ -223,6 +225,7 @@ describe "Admin::Users", feature: true do @simple_user.reload expect(@simple_user.name).to eq('Big Bang') expect(@simple_user.is_admin?).to be_truthy + expect(@simple_user.password_expires_at).to be <= Time.now end end end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 090a941958f..b8ecc356b4d 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -5,8 +5,9 @@ describe "Builds" do before do login_as(:user) - @commit = FactoryGirl.create :ci_commit - @build = FactoryGirl.create :ci_build, commit: @commit + @commit = FactoryGirl.create :ci_pipeline + @build = FactoryGirl.create :ci_build, pipeline: @commit + @build2 = FactoryGirl.create :ci_build @project = @commit.project @project.team << [@user, :developer] end @@ -46,7 +47,7 @@ describe "Builds" do it { expect(page).to have_content @build.short_sha } it { expect(page).to have_content @build.ref } it { expect(page).to have_content @build.name } - it { expect(page).to_not have_link 'Cancel running' } + it { expect(page).not_to have_link 'Cancel running' } end end @@ -62,17 +63,28 @@ describe "Builds" do it { expect(page).to have_content @build.short_sha } it { expect(page).to have_content @build.ref } it { expect(page).to have_content @build.name } - it { expect(page).to_not have_link 'Cancel running' } + it { expect(page).not_to have_link 'Cancel running' } end describe "GET /:project/builds/:id" do - before do - visit namespace_project_build_path(@project.namespace, @project, @build) + context "Build from project" do + before do + visit namespace_project_build_path(@project.namespace, @project, @build) + end + + it { expect(page.status_code).to eq(200) } + it { expect(page).to have_content @commit.sha[0..7] } + it { expect(page).to have_content @commit.git_commit_message } + it { expect(page).to have_content @commit.git_author_name } end - it { expect(page).to have_content @commit.sha[0..7] } - it { expect(page).to have_content @commit.git_commit_message } - it { expect(page).to have_content @commit.git_author_name } + context "Build from other project" do + before do + visit namespace_project_build_path(@project.namespace, @project, @build2) + end + + it { expect(page.status_code).to eq(404) } + end context "Download artifacts" do before do @@ -81,9 +93,7 @@ describe "Builds" do end it 'has button to download artifacts' do - page.within('.artifacts') do - expect(page).to have_content 'Download' - end + expect(page).to have_content 'Download' end end @@ -95,59 +105,144 @@ describe "Builds" do end it do - page.within('.build-controls') do - expect(page).to have_link 'Raw' - end + expect(page).to have_link 'Raw' end end end describe "POST /:project/builds/:id/cancel" do - before do - @build.run! - visit namespace_project_build_path(@project.namespace, @project, @build) - click_link "Cancel" + context "Build from project" do + before do + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + click_link "Cancel" + end + + it { expect(page.status_code).to eq(200) } + it { expect(page).to have_content 'canceled' } + it { expect(page).to have_content 'Retry' } end - it { expect(page).to have_content 'canceled' } - it { expect(page).to have_content 'Retry' } + context "Build from other project" do + before do + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + page.driver.post(cancel_namespace_project_build_path(@project.namespace, @project, @build2)) + end + + it { expect(page.status_code).to eq(404) } + end end describe "POST /:project/builds/:id/retry" do - before do - @build.run! - visit namespace_project_build_path(@project.namespace, @project, @build) - click_link "Cancel" - click_link 'Retry' + context "Build from project" do + before do + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + click_link 'Cancel' + click_link 'Retry' + end + + it { expect(page.status_code).to eq(200) } + it { expect(page).to have_content 'pending' } + it { expect(page).to have_content 'Cancel' } end - it { expect(page).to have_content 'pending' } - it { expect(page).to have_content 'Cancel' } + context "Build from other project" do + before do + @build.run! + visit namespace_project_build_path(@project.namespace, @project, @build) + click_link 'Cancel' + page.driver.post(retry_namespace_project_build_path(@project.namespace, @project, @build2)) + end + + it { expect(page.status_code).to eq(404) } + end end describe "GET /:project/builds/:id/download" do before do @build.update_attributes(artifacts_file: artifacts_file) visit namespace_project_build_path(@project.namespace, @project, @build) - page.within('.artifacts') { click_link 'Download' } + click_link 'Download' end - it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) } + context "Build from other project" do + before do + @build2.update_attributes(artifacts_file: artifacts_file) + visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build2) + end + + it { expect(page.status_code).to eq(404) } + end end describe "GET /:project/builds/:id/raw" do - before do - Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') - @build.run! - @build.trace = 'BUILD TRACE' - visit namespace_project_build_path(@project.namespace, @project, @build) + context "Build from project" do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build.run! + @build.trace = 'BUILD TRACE' + visit namespace_project_build_path(@project.namespace, @project, @build) + page.within('.js-build-sidebar') { click_link 'Raw' } + end + + it 'sends the right headers' do + expect(page.status_code).to eq(200) + expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') + expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace) + end end - it 'sends the right headers' do - page.within('.build-controls') { click_link 'Raw' } + context "Build from other project" do + before do + Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile') + @build2.run! + @build2.trace = 'BUILD TRACE' + visit raw_namespace_project_build_path(@project.namespace, @project, @build2) + puts page.status_code + puts current_url + end + + it 'sends the right headers' do + expect(page.status_code).to eq(404) + end + end + end + + describe "GET /:project/builds/:id/trace.json" do + context "Build from project" do + before do + visit trace_namespace_project_build_path(@project.namespace, @project, @build, format: :json) + end + + it { expect(page.status_code).to eq(200) } + end + + context "Build from other project" do + before do + visit trace_namespace_project_build_path(@project.namespace, @project, @build2, format: :json) + end + + it { expect(page.status_code).to eq(404) } + end + end + + describe "GET /:project/builds/:id/status" do + context "Build from project" do + before do + visit status_namespace_project_build_path(@project.namespace, @project, @build) + end + + it { expect(page.status_code).to eq(200) } + end + + context "Build from other project" do + before do + visit status_namespace_project_build_path(@project.namespace, @project, @build2) + end - expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8') - expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace) + it { expect(page.status_code).to eq(404) } end end end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index dacaa96d760..45e1a157a1f 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -8,15 +8,15 @@ describe 'Commits' do describe 'CI' do before do login_as :user - stub_ci_commit_to_return_yaml_file + stub_ci_pipeline_to_return_yaml_file end - let!(:commit) do - FactoryGirl.create :ci_commit, project: project, sha: project.commit.sha + let!(:pipeline) do + FactoryGirl.create :ci_pipeline, project: project, sha: project.commit.sha end context 'commit status is Generic Commit Status' do - let!(:status) { FactoryGirl.create :generic_commit_status, commit: commit } + let!(:status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline } before do project.team << [@user, :reporter] @@ -24,10 +24,10 @@ describe 'Commits' do describe 'Commit builds' do before do - visit ci_status_path(commit) + visit ci_status_path(pipeline) end - it { expect(page).to have_content commit.sha[0..7] } + it { expect(page).to have_content pipeline.sha[0..7] } it 'contains generic commit status build' do page.within('.table-holder') do @@ -39,7 +39,7 @@ describe 'Commits' do end context 'commit status is Ci Build' do - let!(:build) { FactoryGirl.create :ci_build, commit: commit } + let!(:build) { FactoryGirl.create :ci_build, pipeline: pipeline } let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } context 'when logged as developer' do @@ -53,7 +53,7 @@ describe 'Commits' do end it 'should show build status' do - page.within("//li[@id='commit-#{commit.short_sha}']") do + page.within("//li[@id='commit-#{pipeline.short_sha}']") do expect(page).to have_css(".ci-status-link") end end @@ -61,12 +61,12 @@ describe 'Commits' do describe 'Commit builds' do before do - visit ci_status_path(commit) + visit ci_status_path(pipeline) end - it { expect(page).to have_content commit.sha[0..7] } - it { expect(page).to have_content commit.git_commit_message } - it { expect(page).to have_content commit.git_author_name } + it { expect(page).to have_content pipeline.sha[0..7] } + it { expect(page).to have_content pipeline.git_commit_message } + it { expect(page).to have_content pipeline.git_author_name } end context 'Download artifacts' do @@ -75,7 +75,7 @@ describe 'Commits' do end it do - visit ci_status_path(commit) + visit ci_status_path(pipeline) click_on 'Download artifacts' expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) end @@ -83,7 +83,7 @@ describe 'Commits' do describe 'Cancel all builds' do it 'cancels commit' do - visit ci_status_path(commit) + visit ci_status_path(pipeline) click_on 'Cancel running' expect(page).to have_content 'canceled' end @@ -91,7 +91,7 @@ describe 'Commits' do describe 'Cancel build' do it 'cancels build' do - visit ci_status_path(commit) + visit ci_status_path(pipeline) click_on 'Cancel' expect(page).to have_content 'canceled' end @@ -100,13 +100,13 @@ describe 'Commits' do describe '.gitlab-ci.yml not found warning' do context 'ci builds enabled' do it "does not show warning" do - visit ci_status_path(commit) + visit ci_status_path(pipeline) expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' end it 'shows warning' do - stub_ci_commit_yaml_file(nil) - visit ci_status_path(commit) + stub_ci_pipeline_yaml_file(nil) + visit ci_status_path(pipeline) expect(page).to have_content '.gitlab-ci.yml not found in this commit' end end @@ -114,8 +114,8 @@ describe 'Commits' do context 'ci builds disabled' do before do stub_ci_builds_disabled - stub_ci_commit_yaml_file(nil) - visit ci_status_path(commit) + stub_ci_pipeline_yaml_file(nil) + visit ci_status_path(pipeline) end it 'does not show warning' do @@ -129,16 +129,16 @@ describe 'Commits' do before do project.team << [@user, :reporter] build.update_attributes(artifacts_file: artifacts_file) - visit ci_status_path(commit) + visit ci_status_path(pipeline) end it do - expect(page).to have_content commit.sha[0..7] - expect(page).to have_content commit.git_commit_message - expect(page).to have_content commit.git_author_name + expect(page).to have_content pipeline.sha[0..7] + expect(page).to have_content pipeline.git_commit_message + expect(page).to have_content pipeline.git_author_name expect(page).to have_link('Download artifacts') - expect(page).to_not have_link('Cancel running') - expect(page).to_not have_link('Retry failed') + expect(page).not_to have_link('Cancel running') + expect(page).not_to have_link('Retry failed') end end @@ -148,16 +148,16 @@ describe 'Commits' do visibility_level: Gitlab::VisibilityLevel::INTERNAL, public_builds: false) build.update_attributes(artifacts_file: artifacts_file) - visit ci_status_path(commit) + visit ci_status_path(pipeline) end it do - expect(page).to have_content commit.sha[0..7] - expect(page).to have_content commit.git_commit_message - expect(page).to have_content commit.git_author_name - expect(page).to_not have_link('Download artifacts') - expect(page).to_not have_link('Cancel running') - expect(page).to_not have_link('Retry failed') + expect(page).to have_content pipeline.sha[0..7] + expect(page).to have_content pipeline.git_commit_message + expect(page).to have_content pipeline.git_author_name + expect(page).not_to have_link('Download artifacts') + expect(page).not_to have_link('Cancel running') + expect(page).not_to have_link('Retry failed') end end end diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb new file mode 100644 index 00000000000..53b4f027117 --- /dev/null +++ b/spec/features/container_registry_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe "Container Registry" do + let(:project) { create(:empty_project) } + let(:repository) { project.container_registry_repository } + let(:tag_name) { 'latest' } + let(:tags) { [tag_name] } + + before do + login_as(:user) + project.team << [@user, :developer] + stub_container_registry_tags(*tags) + stub_container_registry_config(enabled: true) + allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token') + end + + describe 'GET /:project/container_registry' do + before do + visit namespace_project_container_registry_index_path(project.namespace, project) + end + + context 'when no tags' do + let(:tags) { [] } + + it { expect(page).to have_content('No images in Container Registry for this project') } + end + + context 'when there are tags' do + it { expect(page).to have_content(tag_name)} + end + end + + describe 'DELETE /:project/container_registry/tag' do + before do + visit namespace_project_container_registry_index_path(project.namespace, project) + end + + it do + expect_any_instance_of(::ContainerRegistry::Tag).to receive(:delete).and_return(true) + + click_on 'Remove' + end + end +end diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb new file mode 100644 index 00000000000..365cb445df1 --- /dev/null +++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +feature 'Tooltips on .timeago dates', feature: true, js: true do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:project, name: 'test', namespace: user.namespace) } + let(:created_date) { Date.yesterday.to_time } + let(:expected_format) { created_date.strftime('%b %-d, %Y %l:%M%P UTC') } + + context 'on the activity tab' do + before do + project.team << [user, :master] + + Event.create( project: project, author_id: user.id, action: Event::JOINED, + updated_at: created_date, created_at: created_date) + + login_as user + visit user_path(user) + wait_for_ajax() + + page.find('.js-timeago').hover + end + + it 'has the datetime formated correctly' do + expect(page).to have_selector('.local-timeago', text: expected_format) + end + end + + context 'on the snippets tab' do + before do + project.team << [user, :master] + create(:snippet, author: user, updated_at: created_date, created_at: created_date) + + login_as user + visit user_snippets_path(user) + wait_for_ajax() + + page.find('.js-timeago').hover + end + + it 'has the datetime formated correctly' do + expect(page).to have_selector('.local-timeago', text: expected_format) + end + end +end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index 41af789aae2..07a854ea014 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -28,7 +28,6 @@ describe 'Awards Emoji', feature: true do end context 'click the thumbsup emoji' do - it 'should increment the thumbsup emoji', js: true do find('[data-emoji="thumbsup"]').click sleep 2 @@ -41,7 +40,6 @@ describe 'Awards Emoji', feature: true do end context 'click the thumbsdown emoji' do - it 'should increment the thumbsdown emoji', js: true do find('[data-emoji="thumbsdown"]').click sleep 2 diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb new file mode 100644 index 00000000000..63efecf8780 --- /dev/null +++ b/spec/features/issues/award_spec.rb @@ -0,0 +1,49 @@ +require 'rails_helper' + +feature 'Issue awards', js: true, feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + + describe 'logged in' do + before do + login_as(user) + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'should add award to issue' do + first('.js-emoji-btn').click + expect(page).to have_selector('.js-emoji-btn.active') + expect(first('.js-emoji-btn')).to have_content '1' + + visit namespace_project_issue_path(project.namespace, project, issue) + expect(first('.js-emoji-btn')).to have_content '1' + end + + it 'should remove award from issue' do + first('.js-emoji-btn').click + find('.js-emoji-btn.active').click + expect(first('.js-emoji-btn')).to have_content '0' + + visit namespace_project_issue_path(project.namespace, project, issue) + expect(first('.js-emoji-btn')).to have_content '0' + end + + it 'should only have one menu on the page' do + first('.js-add-award').click + expect(page).to have_selector('.emoji-menu') + + expect(page).to have_selector('.emoji-menu', count: 1) + end + end + + describe 'logged out' do + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'should not see award menu button' do + expect(page).not_to have_selector('.js-award-holder') + end + end +end diff --git a/spec/features/issues/bulk_assigment_labels_spec.rb b/spec/features/issues/bulk_assigment_labels_spec.rb new file mode 100644 index 00000000000..c58b87281a3 --- /dev/null +++ b/spec/features/issues/bulk_assigment_labels_spec.rb @@ -0,0 +1,196 @@ +require 'rails_helper' + +feature 'Issues > Labels bulk assignment', feature: true do + include WaitForAjax + + let(:user) { create(:user) } + let!(:project) { create(:project) } + let!(:issue1) { create(:issue, project: project, title: "Issue 1") } + let!(:issue2) { create(:issue, project: project, title: "Issue 2") } + let!(:bug) { create(:label, project: project, title: 'bug') } + let!(:feature) { create(:label, project: project, title: 'feature') } + + context 'as a allowed user', js: true do + before do + project.team << [user, :master] + + login_as user + end + + context 'can bulk assign' do + before do + visit namespace_project_issues_path(project.namespace, project) + end + + context 'a label' do + context 'to all issues' do + before do + check 'check_all_issues' + open_labels_dropdown ['bug'] + update_issues + end + + it do + expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue2.id}")).to have_content 'bug' + end + end + + context 'to a issue' do + before do + check "selected_issue_#{issue1.id}" + open_labels_dropdown ['bug'] + update_issues + end + + it do + expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + end + end + end + + context 'multiple labels' do + context 'to all issues' do + before do + check 'check_all_issues' + open_labels_dropdown ['bug', 'feature'] + update_issues + end + + it do + expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue1.id}")).to have_content 'feature' + expect(find("#issue_#{issue2.id}")).to have_content 'bug' + expect(find("#issue_#{issue2.id}")).to have_content 'feature' + end + end + + context 'to a issue' do + before do + check "selected_issue_#{issue1.id}" + open_labels_dropdown ['bug', 'feature'] + update_issues + end + + it do + expect(find("#issue_#{issue1.id}")).to have_content 'bug' + expect(find("#issue_#{issue1.id}")).to have_content 'feature' + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue2.id}")).not_to have_content 'feature' + end + end + end + end + + context 'can bulk un-assign' do + context 'all labels to all issues' do + before do + issue1.labels << bug + issue1.labels << feature + issue2.labels << bug + issue2.labels << feature + + visit namespace_project_issues_path(project.namespace, project) + + check 'check_all_issues' + unmark_labels_in_dropdown ['bug', 'feature'] + update_issues + end + + it do + expect(find("#issue_#{issue1.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue1.id}")).not_to have_content 'feature' + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue2.id}")).not_to have_content 'feature' + end + end + + context 'a label to a issue' do + before do + issue1.labels << bug + issue2.labels << feature + + visit namespace_project_issues_path(project.namespace, project) + + check_issue issue1 + unmark_labels_in_dropdown ['bug'] + update_issues + end + + it do + expect(find("#issue_#{issue1.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue2.id}")).to have_content 'feature' + end + end + + context 'a label and keep the others label' do + before do + issue1.labels << bug + issue1.labels << feature + issue2.labels << bug + issue2.labels << feature + + visit namespace_project_issues_path(project.namespace, project) + + check_issue issue1 + check_issue issue2 + unmark_labels_in_dropdown ['bug'] + update_issues + end + + it do + expect(find("#issue_#{issue1.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue1.id}")).to have_content 'feature' + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue2.id}")).to have_content 'feature' + end + end + end + end + + context 'as a guest' do + before do + login_as user + + visit namespace_project_issues_path(project.namespace, project) + end + + context 'cannot bulk assign labels' do + it do + expect(page).not_to have_css '.check_all_issues' + expect(page).not_to have_css '.issue-check' + end + end + end + + def open_labels_dropdown(items = [], unmark = false) + page.within('.issues_bulk_update') do + click_button 'Label' + wait_for_ajax + items.map do |item| + click_link item + end + if unmark + items.map do |item| + click_link item + end + end + end + end + + def unmark_labels_in_dropdown(items = []) + open_labels_dropdown(items, true) + end + + def check_issue(issue) + page.within('.issues-list') do + check "selected_issue_#{issue.id}" + end + end + + def update_issues + click_button 'Update issues' + wait_for_ajax + end +end diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb index 7f654684143..16c619c9288 100644 --- a/spec/features/issues/filter_by_labels_spec.rb +++ b/spec/features/issues/filter_by_labels_spec.rb @@ -54,6 +54,11 @@ feature 'Issue filtering by Labels', feature: true do expect(find('.filtered-labels')).not_to have_content "feature" expect(find('.filtered-labels')).not_to have_content "enhancement" end + + it 'should remove label "bug"' do + first('.js-label-filter-remove').click + expect(find('.filtered-labels')).to have_no_content "bug" + end end context 'filter by label feature', js: true do @@ -135,6 +140,11 @@ feature 'Issue filtering by Labels', feature: true do it 'should not show label "bug" in filtered-labels' do expect(find('.filtered-labels')).not_to have_content "bug" end + + it 'should remove label "enhancement"' do + first('.js-label-filter-remove').click + expect(find('.filtered-labels')).to have_no_content "enhancement" + end end context 'filter by label enhancement and bug in issues list', js: true do @@ -164,4 +174,44 @@ feature 'Issue filtering by Labels', feature: true do expect(find('.filtered-labels')).not_to have_content "feature" end end + + context 'remove filtered labels', js: true do + before do + page.within '.labels-filter' do + click_button 'Label' + click_link 'bug' + find('.dropdown-menu-close').click + end + + page.within '.filtered-labels' do + expect(page).to have_content 'bug' + end + end + + it 'should allow user to remove filtered labels' do + page.within '.filtered-labels' do + first('.js-label-filter-remove').click + expect(page).not_to have_content 'bug' + end + + page.within '.labels-filter' do + expect(page).not_to have_content 'bug' + end + end + end + + context 'dropdown filtering', js: true do + it 'should filter by label name' do + page.within '.labels-filter' do + click_button 'Label' + wait_for_ajax + fill_in 'label-name', with: 'bug' + + page.within '.dropdown-content' do + expect(page).not_to have_content 'enhancement' + expect(page).to have_content 'bug' + end + end + end + end end diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 192e3619375..1f0594e6b02 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -154,4 +154,144 @@ describe 'Filter issues', feature: true do end end end + + describe 'filter issues by text' do + before do + create(:issue, title: "Bug", project: project) + + bug_label = create(:label, project: project, title: 'bug') + milestone = create(:milestone, title: "8", project: project) + + issue = create(:issue, + title: "Bug 2", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue.labels << bug_label + + visit namespace_project_issues_path(project.namespace, project) + end + + context 'only text', js: true do + it 'should filter issues by searched text' do + fill_in 'issue_search', with: 'Bug' + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 2) + end + end + + it 'should not show any issues' do + fill_in 'issue_search', with: 'testing' + + page.within '.issues-list' do + expect(page).not_to have_selector('.issue') + end + end + end + + context 'text and dropdown options', js: true do + it 'should filter by text and label' do + fill_in 'issue_search', with: 'Bug' + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 2) + end + + click_button 'Label' + page.within '.labels-filter' do + click_link 'bug' + end + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 1) + end + end + + it 'should filter by text and milestone' do + fill_in 'issue_search', with: 'Bug' + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 2) + end + + click_button 'Milestone' + page.within '.milestone-filter' do + click_link '8' + end + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 1) + end + end + + it 'should filter by text and assignee' do + fill_in 'issue_search', with: 'Bug' + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 2) + end + + click_button 'Assignee' + page.within '.dropdown-menu-assignee' do + click_link user.name + end + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 1) + end + end + + it 'should filter by text and author' do + fill_in 'issue_search', with: 'Bug' + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 2) + end + + click_button 'Author' + page.within '.dropdown-menu-author' do + click_link user.name + end + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 1) + end + end + end + end + + describe 'filter issues and sort', js: true do + before do + bug_label = create(:label, project: project, title: 'bug') + bug_one = create(:issue, title: "Frontend", project: project) + bug_two = create(:issue, title: "Bug 2", project: project) + + bug_one.labels << bug_label + bug_two.labels << bug_label + + visit namespace_project_issues_path(project.namespace, project) + end + + it 'should be able to filter and sort issues' do + click_button 'Label' + page.within '.labels-filter' do + click_link 'bug' + end + + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: 2) + end + + click_button 'Last created' + page.within '.dropdown-menu-sort' do + click_link 'Oldest created' + end + + page.within '.issues-list' do + expect(first('.issue')).to have_content('Frontend') + end + end + end end diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index 84c8e20ebaa..c7019c5aea1 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -19,7 +19,7 @@ feature 'issue move to another project' do end scenario 'moving issue to another project not allowed' do - expect(page).to have_no_select('move_to_project_id') + expect(page).to have_no_selector('#move_to_project_id') end end @@ -37,7 +37,7 @@ feature 'issue move to another project' do end scenario 'moving issue to another project' do - select(new_project.name_with_namespace, from: 'move_to_project_id') + first('#move_to_project_id', visible: false).set(new_project.id) click_button('Save changes') expect(current_url).to include project_path(new_project) @@ -47,14 +47,18 @@ feature 'issue move to another project' do expect(page).to have_content(issue.title) end - context 'projects user does not have permission to move issue to exist' do + context 'user does not have permission to move the issue to a project', js: true do let!(:private_project) { create(:project, :private) } let(:another_project) { create(:project) } background { another_project.team << [user, :guest] } scenario 'browsing projects in projects select' do - options = [ '', 'No project', new_project.name_with_namespace ] - expect(page).to have_select('move_to_project_id', options: options) + click_link 'Select project' + + page.within '.select2-results' do + expect(page).to have_content 'No project' + expect(page).to have_content new_project.name_with_namespace + end end end @@ -65,7 +69,7 @@ feature 'issue move to another project' do end scenario 'user wants to move issue that has already been moved' do - expect(page).to have_no_select('move_to_project_id') + expect(page).to have_no_selector('#move_to_project_id') end end end diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index e4efdbe2421..f5cfe2d666e 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -9,8 +9,11 @@ feature 'Issue notes polling' do end scenario 'Another user adds a comment to an issue', js: true do - note = create(:note_on_issue, noteable: issue, note: 'Looks good!') + note = create(:note, noteable: issue, project: project, + note: 'Looks good!') + page.execute_script('notes.refresh();') + expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!') end end diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index b03dd0f666d..ddbd69b2891 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' feature 'Multiple issue updating from issues#index', feature: true do + include WaitForAjax + let!(:project) { create(:project) } let!(:issue) { create(:issue, project: project) } let!(:user) { create(:user)} @@ -24,9 +26,7 @@ feature 'Multiple issue updating from issues#index', feature: true do it 'should be set to open' do create_closed - visit namespace_project_issues_path(project.namespace, project) - - find('.issues-state-filters a', text: 'Closed').click + visit namespace_project_issues_path(project.namespace, project, state: 'closed') find('#check_all_issues').click find('.js-issue-status').click @@ -42,7 +42,7 @@ feature 'Multiple issue updating from issues#index', feature: true do visit namespace_project_issues_path(project.namespace, project) find('#check_all_issues').click - find('.js-update-assignee').click + click_update_assignee_button find('.dropdown-menu-user-link', text: user.username).click click_update_issues_button @@ -57,14 +57,11 @@ feature 'Multiple issue updating from issues#index', feature: true do visit namespace_project_issues_path(project.namespace, project) find('#check_all_issues').click - find('.js-update-assignee').click + click_update_assignee_button click_link 'Unassigned' click_update_issues_button - - within first('.issue .controls') do - expect(page).to have_no_selector('.author_link') - end + expect(find('.issue:first-child .controls')).not_to have_css('.author_link') end end @@ -95,7 +92,7 @@ feature 'Multiple issue updating from issues#index', feature: true do find('.dropdown-menu-milestone a', text: "No Milestone").click click_update_issues_button - expect(first('.issue')).to_not have_content milestone.title + expect(find('.issue:first-child')).not_to have_content milestone.title end end @@ -111,7 +108,13 @@ feature 'Multiple issue updating from issues#index', feature: true do create(:issue, project: project, milestone: milestone) end + def click_update_assignee_button + find('.js-update-assignee').click + wait_for_ajax + end + def click_update_issues_button find('.update_selected_issues').click + wait_for_ajax end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index d5755c293c5..f6fb6a72d22 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -64,10 +64,70 @@ describe 'Issues', feature: true do end end + describe 'due date', js: true do + context 'on new form' do + before do + visit new_namespace_project_issue_path(project.namespace, project) + end + + it 'should save with due date' do + date = Date.today.at_beginning_of_month + + fill_in 'issue_title', with: 'bug 345' + fill_in 'issue_description', with: 'bug description' + find('#issuable-due-date').click + + page.within '.ui-datepicker' do + click_link date.day + end + + expect(find('#issuable-due-date').value).to eq date.to_s + + click_button 'Submit issue' + + page.within '.issuable-sidebar' do + expect(page).to have_content date.to_s(:medium) + end + end + end + + context 'on edit form' do + let(:issue) { create(:issue, author: @user,project: project, due_date: Date.today.at_beginning_of_month.to_s) } + + before do + visit edit_namespace_project_issue_path(project.namespace, project, issue) + end + + it 'should save with due date' do + date = Date.today.at_beginning_of_month + + expect(find('#issuable-due-date').value).to eq date.to_s + + date = date.tomorrow + + fill_in 'issue_title', with: 'bug 345' + fill_in 'issue_description', with: 'bug description' + find('#issuable-due-date').click + + page.within '.ui-datepicker' do + click_link date.day + end + + expect(find('#issuable-due-date').value).to eq date.to_s + + click_button 'Save changes' + + page.within '.issuable-sidebar' do + expect(page).to have_content date.to_s(:medium) + end + end + end + end + describe 'Issue info' do it 'excludes award_emoji from comment count' do issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar') - create(:upvote_note, noteable: issue) + create(:award_emoji, awardable: issue) visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id) @@ -307,13 +367,9 @@ describe 'Issues', feature: true do page.within('.assignee') do expect(page).to have_content "#{@user.name}" - end - find('.block.assignee .edit-link').click - sleep 2 # wait for ajax stuff to complete - first('.dropdown-menu-user-link').click - sleep 2 - page.within('.assignee') do + click_link 'Edit' + click_link 'Unassigned' expect(page).to have_content 'No assignee' end @@ -331,7 +387,7 @@ describe 'Issues', feature: true do page.within '.assignee' do click_link 'Edit' end - + page.within '.dropdown-menu-user' do click_link @user.name end @@ -431,6 +487,43 @@ describe 'Issues', feature: true do end end + describe 'due date' do + context 'update due on issue#show', js: true do + let(:issue) { create(:issue, project: project, author: @user, assignee: @user) } + + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'should add due date to issue' do + page.within '.due_date' do + click_link 'Edit' + + page.within '.ui-datepicker-calendar' do + first('.ui-state-default').click + end + + expect(page).to have_no_content 'None' + end + end + + it 'should remove due date from issue' do + page.within '.due_date' do + click_link 'Edit' + + page.within '.ui-datepicker-calendar' do + first('.ui-state-default').click + end + + expect(page).to have_no_content 'None' + + click_link 'remove due date' + expect(page).to have_content 'None' + end + end + end + end + def first_issue page.all('ul.issues-list > li').first.text end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index 8c38dd5b122..72b5ff231f7 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -32,12 +32,12 @@ feature 'Login', feature: true do let(:user) { create(:user, :two_factor) } before do - login_with(user) - expect(page).to have_content('Two-factor Authentication') + login_with(user, remember: true) + expect(page).to have_content('Two-Factor Authentication') end def enter_code(code) - fill_in 'Two-factor Authentication code', with: code + fill_in 'Two-Factor Authentication code', with: code click_button 'Verify code' end @@ -52,6 +52,12 @@ feature 'Login', feature: true do expect(current_path).to eq root_path end + it 'persists remember_me value via hidden field' do + field = first('input#user_remember_me', visible: false) + + expect(field.value).to eq '1' + end + it 'blocks login with invalid code' do enter_code('foo') expect(page).to have_content('Invalid two-factor code') @@ -121,7 +127,7 @@ feature 'Login', feature: true do user = create(:user, password: 'not-the-default') login_with(user) - expect(page).to have_content('Invalid login or password.') + expect(page).to have_content('Invalid Login or password.') end end @@ -137,12 +143,12 @@ feature 'Login', feature: true do context 'within the grace period' do it 'redirects to two-factor configuration page' do - expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must enable Two-factor Authentication for your account before') + expect(current_path).to eq profile_two_factor_auth_path + expect(page).to have_content('You must enable Two-Factor Authentication for your account before') end - it 'disallows skipping two-factor configuration' do - expect(current_path).to eq new_profile_two_factor_auth_path + it 'allows skipping two-factor configuration', js: true do + expect(current_path).to eq profile_two_factor_auth_path click_link 'Configure it later' expect(current_path).to eq root_path @@ -153,26 +159,26 @@ feature 'Login', feature: true do let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) } it 'redirects to two-factor configuration page' do - expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must enable Two-factor Authentication for your account.') + expect(current_path).to eq profile_two_factor_auth_path + expect(page).to have_content('You must enable Two-Factor Authentication for your account.') end - it 'disallows skipping two-factor configuration' do - expect(current_path).to eq new_profile_two_factor_auth_path + it 'disallows skipping two-factor configuration', js: true do + expect(current_path).to eq profile_two_factor_auth_path expect(page).not_to have_link('Configure it later') end end end - context 'without grace pariod defined' do + context 'without grace period defined' do before(:each) do stub_application_setting(two_factor_grace_period: 0) login_with(user) end it 'redirects to two-factor configuration page' do - expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must enable Two-factor Authentication for your account.') + expect(current_path).to eq profile_two_factor_auth_path + expect(page).to have_content('You must enable Two-Factor Authentication for your account.') end end end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index 0148c87084a..09ccc77c101 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -165,22 +165,32 @@ describe 'GitLab Markdown', feature: true do describe 'ExternalLinkFilter' do it 'adds nofollow to external link' do link = doc.at_css('a:contains("Google")') + expect(link.attr('rel')).to include('nofollow') end it 'adds noreferrer to external link' do link = doc.at_css('a:contains("Google")') + expect(link.attr('rel')).to include('noreferrer') end + it 'adds _blank to target attribute for external links' do + link = doc.at_css('a:contains("Google")') + + expect(link.attr('target')).to match('_blank') + end + it 'ignores internal link' do link = doc.at_css('a:contains("GitLab Root")') + expect(link.attr('rel')).not_to match 'nofollow' + expect(link.attr('target')).not_to match '_blank' end end end - before(:all) do + before do @feat = MarkdownFeature.new # `markdown` helper expects a `@project` variable @@ -188,7 +198,7 @@ describe 'GitLab Markdown', feature: true do end context 'default pipeline' do - before(:all) do + before do @html = markdown(@feat.raw_markdown) end @@ -231,13 +241,14 @@ describe 'GitLab Markdown', feature: true do context 'wiki pipeline' do before do @project_wiki = @feat.project_wiki + @project_wiki_page = @feat.project_wiki_page file = Gollum::File.new(@project_wiki.wiki) expect(file).to receive(:path).and_return('images/example.jpg') expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file) allow(@project_wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' } - @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki }) + @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki, page_slug: @project_wiki_page.slug }) end it_behaves_like 'all pipelines' @@ -278,6 +289,10 @@ describe 'GitLab Markdown', feature: true do it 'includes GollumTagsFilter' do expect(doc).to parse_gollum_tags end + + it 'includes InlineDiffFilter' do + expect(doc).to parse_inline_diffs + end end # Fake a `current_user` helper diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_requests/award_spec.rb new file mode 100644 index 00000000000..007f67d6080 --- /dev/null +++ b/spec/features/merge_requests/award_spec.rb @@ -0,0 +1,49 @@ +require 'rails_helper' + +feature 'Merge request awards', js: true, feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request, source_project: project) } + + describe 'logged in' do + before do + login_as(user) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'should add award to merge request' do + first('.js-emoji-btn').click + expect(page).to have_selector('.js-emoji-btn.active') + expect(first('.js-emoji-btn')).to have_content '1' + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + expect(first('.js-emoji-btn')).to have_content '1' + end + + it 'should remove award from merge request' do + first('.js-emoji-btn').click + find('.js-emoji-btn.active').click + expect(first('.js-emoji-btn')).to have_content '0' + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + expect(first('.js-emoji-btn')).to have_content '0' + end + + it 'should only have one menu on the page' do + first('.js-add-award').click + expect(page).to have_selector('.emoji-menu') + + expect(page).to have_selector('.emoji-menu', count: 1) + end + end + + describe 'logged out' do + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'should not see award menu button' do + expect(page).not_to have_selector('.js-award-holder') + end + end +end diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb new file mode 100644 index 00000000000..b4d2201c729 --- /dev/null +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +feature 'Merge request created from fork' do + given(:user) { create(:user) } + given(:project) { create(:project, :public) } + given(:fork_project) { create(:project, :public) } + + given!(:merge_request) do + create(:forked_project_link, forked_to_project: fork_project, + forked_from_project: project) + + create(:merge_request_with_diffs, source_project: fork_project, + target_project: project, + description: 'Test merge request') + end + + background do + fork_project.team << [user, :master] + login_as user + end + + scenario 'user can access merge request' do + visit_merge_request(merge_request) + + expect(page).to have_content 'Test merge request' + end + + context 'pipeline present in source project' do + include WaitForAjax + + given(:pipeline) do + create(:ci_pipeline_with_two_job, project: fork_project, + sha: merge_request.last_commit.id, + ref: merge_request.source_branch) + end + + background { pipeline.create_builds(user) } + + scenario 'user visits a pipelines page', js: true do + visit_merge_request(merge_request) + page.within('.merge-request-tabs') { click_link 'Builds' } + wait_for_ajax + + page.within('table.builds') do + expect(page).to have_content 'rspec' + expect(page).to have_content 'spinach' + end + + expect(find_link('Cancel running')[:href]) + .to include fork_project.path_with_namespace + end + end + + def visit_merge_request(mr) + visit namespace_project_merge_request_path(project.namespace, + project, mr) + end +end diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index 7aa7eb965e9..c5e6412d7bf 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -12,8 +12,8 @@ feature 'Merge When Build Succeeds', feature: true, js: true do end context "Active build for Merge Request" do - let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } - let!(:ci_build) { create(:ci_build, commit: ci_commit) } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + let!(:ci_build) { create(:ci_build, pipeline: pipeline) } before do login_as user @@ -47,8 +47,8 @@ feature 'Merge When Build Succeeds', feature: true, js: true do merge_user: user, title: "MepMep", merge_when_build_succeeds: true) end - let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } - let!(:ci_build) { create(:ci_build, commit: ci_commit) } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + let!(:ci_build) { create(:ci_build, pipeline: pipeline) } before do login_as user diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb new file mode 100644 index 00000000000..65e9185ec24 --- /dev/null +++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb @@ -0,0 +1,105 @@ +require 'spec_helper' + +feature 'Only allow merge requests to be merged if the build succeeds', feature: true do + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request_with_diffs, source_project: project) } + + before do + login_as merge_request.author + + project.team << [merge_request.author, :master] + end + + context 'project does not have CI enabled' do + it 'allows MR to be merged' do + visit_merge_request(merge_request) + + expect(page).to have_button 'Accept Merge Request' + end + end + + context 'when project has CI enabled' do + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + + context 'when merge requests can only be merged if the build succeeds' do + before do + project.update_attribute(:only_allow_merge_if_build_succeeds, true) + end + + context 'when CI is running' do + before { pipeline.update_column(:status, :running) } + + it 'does not allow to merge immediately' do + visit_merge_request(merge_request) + + expect(page).to have_button 'Merge When Build Succeeds' + expect(page).not_to have_button 'Select Merge Moment' + end + end + + context 'when CI failed' do + before { pipeline.update_column(:status, :failed) } + + it 'does not allow MR to be merged' do + visit_merge_request(merge_request) + + expect(page).not_to have_button 'Accept Merge Request' + expect(page).to have_content('Please retry the build or push a new commit to fix the failure.') + end + end + + context 'when CI succeeded' do + before { pipeline.update_column(:status, :success) } + + it 'allows MR to be merged' do + visit_merge_request(merge_request) + + expect(page).to have_button 'Accept Merge Request' + end + end + end + + context 'when merge requests can be merged when the build failed' do + before do + project.update_attribute(:only_allow_merge_if_build_succeeds, false) + end + + context 'when CI is running' do + before { pipeline.update_column(:status, :running) } + + it 'allows MR to be merged immediately', js: true do + visit_merge_request(merge_request) + + expect(page).to have_button 'Merge When Build Succeeds' + + click_button 'Select Merge Moment' + expect(page).to have_content 'Merge Immediately' + end + end + + context 'when CI failed' do + before { pipeline.update_column(:status, :failed) } + + it 'allows MR to be merged' do + visit_merge_request(merge_request) + + expect(page).to have_button 'Accept Merge Request' + end + end + + context 'when CI succeeded' do + before { pipeline.update_column(:status, :success) } + + it 'allows MR to be merged' do + visit_merge_request(merge_request) + + expect(page).to have_button 'Accept Merge Request' + end + end + end + end + + def visit_merge_request(merge_request) + visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request) + end +end diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb new file mode 100644 index 00000000000..1c130057c56 --- /dev/null +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -0,0 +1,161 @@ +require 'spec_helper' + +describe 'Projects > Merge requests > User lists merge requests', feature: true do + include SortingHelper + + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + + before do + @fix = create(:merge_request, + title: 'fix', + source_project: project, + source_branch: 'fix', + assignee: user, + milestone: create(:milestone, due_date: '2013-12-11'), + created_at: 1.minute.ago, + updated_at: 1.minute.ago) + create(:merge_request, + title: 'markdown', + source_project: project, + source_branch: 'markdown', + assignee: user, + milestone: create(:milestone, due_date: '2013-12-12'), + created_at: 2.minutes.ago, + updated_at: 2.minutes.ago) + create(:merge_request, + title: 'lfs', + source_project: project, + source_branch: 'lfs', + created_at: 3.minutes.ago, + updated_at: 10.seconds.ago) + end + + it 'filters on no assignee' do + visit_merge_requests(project, assignee_id: IssuableFinder::NONE) + + expect(current_path).to eq(namespace_project_merge_requests_path(project.namespace, project)) + expect(page).to have_content 'lfs' + expect(page).not_to have_content 'fix' + expect(page).not_to have_content 'markdown' + expect(count_merge_requests).to eq(1) + end + + it 'filters on a specific assignee' do + visit_merge_requests(project, assignee_id: user.id) + + expect(page).not_to have_content 'lfs' + expect(page).to have_content 'fix' + expect(page).to have_content 'markdown' + expect(count_merge_requests).to eq(2) + end + + it 'sorts by newest' do + visit_merge_requests(project, sort: sort_value_recently_created) + + expect(first_merge_request).to include('lfs') + expect(last_merge_request).to include('fix') + expect(count_merge_requests).to eq(3) + end + + it 'sorts by oldest' do + visit_merge_requests(project, sort: sort_value_oldest_created) + + expect(first_merge_request).to include('fix') + expect(last_merge_request).to include('lfs') + expect(count_merge_requests).to eq(3) + end + + it 'sorts by last updated' do + visit_merge_requests(project, sort: sort_value_recently_updated) + + expect(first_merge_request).to include('lfs') + expect(count_merge_requests).to eq(3) + end + + it 'sorts by oldest updated' do + visit_merge_requests(project, sort: sort_value_oldest_updated) + + expect(first_merge_request).to include('markdown') + expect(count_merge_requests).to eq(3) + end + + it 'sorts by milestone due soon' do + visit_merge_requests(project, sort: sort_value_milestone_soon) + + expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(3) + end + + it 'sorts by milestone due later' do + visit_merge_requests(project, sort: sort_value_milestone_later) + + expect(first_merge_request).to include('markdown') + expect(count_merge_requests).to eq(3) + end + + it 'filters on one label and sorts by due soon' do + label = create(:label, project: project) + create(:label_link, label: label, target: @fix) + + visit_merge_requests(project, label_name: [label.name], + sort: sort_value_due_date_soon) + + expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(1) + end + + context 'while filtering on two labels' do + let(:label) { create(:label, project: project) } + let(:label2) { create(:label, project: project) } + + before do + create(:label_link, label: label, target: @fix) + create(:label_link, label: label2, target: @fix) + end + + it 'sorts by due soon' do + visit_merge_requests(project, label_name: [label.name, label2.name], + sort: sort_value_due_date_soon) + + expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(1) + end + + context 'filter on assignee and' do + it 'sorts by due soon' do + visit_merge_requests(project, label_name: [label.name, label2.name], + assignee_id: user.id, + sort: sort_value_due_date_soon) + + expect(first_merge_request).to include('fix') + expect(count_merge_requests).to eq(1) + end + + it 'sorts by recently due milestone' do + visit namespace_project_merge_requests_path(project.namespace, project, + label_name: [label.name, label2.name], + assignee_id: user.id, + sort: sort_value_milestone_soon) + + expect(first_merge_request).to include('fix') + end + end + end + + def visit_merge_requests(project, opts = {}) + visit namespace_project_merge_requests_path(project.namespace, project, opts) + end + + def first_merge_request + page.all('ul.mr-list > li').first.text + end + + def last_merge_request + page.all('ul.mr-list > li').last.text + end + + def count_merge_requests + page.all('ul.mr-list > li').count + end +end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 389812ff7e1..737efcef45d 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -4,25 +4,15 @@ describe 'Comments', feature: true do include RepoHelpers include WaitForAjax - describe 'On merge requests page', feature: true do - it 'excludes award_emoji from comment count' do - merge_request = create(:merge_request) - project = merge_request.source_project - create(:upvote_note, noteable: merge_request, project: project) - - login_as :admin - visit namespace_project_merge_requests_path(project.namespace, project) - - expect(merge_request.mr_and_commit_notes.count).to eq 1 - expect(page.all('.merge-request-no-comments').first.text).to eq "0" + describe 'On a merge request', js: true, feature: true do + let!(:project) { create(:project) } + let!(:merge_request) do + create(:merge_request, source_project: project, target_project: project) end - end - describe 'On a merge request', js: true, feature: true do - let!(:merge_request) { create(:merge_request) } - let!(:project) { merge_request.source_project } let!(:note) do - create(:note_on_merge_request, :with_attachment, project: project) + create(:note_on_merge_request, :with_attachment, noteable: merge_request, + project: project) end before do @@ -143,17 +133,6 @@ describe 'Comments', feature: true do end end end - - describe 'comment info' do - it 'excludes award_emoji from comment count' do - create(:upvote_note, noteable: merge_request, project: project) - - visit namespace_project_merge_request_path(project.namespace, project, merge_request) - - expect(merge_request.mr_and_commit_notes.count).to eq 2 - expect(find('.notes-tab span.badge').text).to eq "1" - end - end end describe 'On a merge request diff', js: true, feature: true do @@ -192,7 +171,7 @@ describe 'Comments', feature: true do end it 'should be removed when canceled' do - page.within(".diff-file form[id$='#{line_code}']") do + page.within(".diff-file form[id$='#{line_code}-true']") do find('.js-close-discussion-note-form').trigger('click') end diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb index 1adab7e9c6c..c7c00a3266a 100644 --- a/spec/features/participants_autocomplete_spec.rb +++ b/spec/features/participants_autocomplete_spec.rb @@ -32,7 +32,8 @@ feature 'Member autocomplete', feature: true do context 'adding a new note on a Issue', js: true do before do issue = create(:issue, author: author, project: project) - create(:note, note: 'Ultralight Beam', noteable: issue, author: participant) + create(:note, note: 'Ultralight Beam', noteable: issue, + project: project, author: participant) visit_issue(project, issue) end @@ -47,7 +48,8 @@ feature 'Member autocomplete', feature: true do context 'adding a new note on a Merge Request ', js: true do before do merge = create(:merge_request, source_project: project, target_project: project, author: author) - create(:note, note: 'Ultralight Beam', noteable: merge, author: participant) + create(:note, note: 'Ultralight Beam', noteable: merge, + project: project, author: participant) visit_merge_request(project, merge) end diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb new file mode 100644 index 00000000000..98703ef3ac4 --- /dev/null +++ b/spec/features/pipelines_spec.rb @@ -0,0 +1,189 @@ +require 'spec_helper' + +describe "Pipelines" do + include GitlabRoutingHelper + + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + + before do + login_as(user) + project.team << [user, :developer] + end + + describe 'GET /:project/pipelines' do + let!(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', status: 'running') } + + [:all, :running, :branches].each do |scope| + context "displaying #{scope}" do + let(:project) { create(:project) } + + before { visit namespace_project_pipelines_path(project.namespace, project, scope: scope) } + + it { expect(page).to have_content(pipeline.short_sha) } + end + end + + context 'anonymous access' do + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).to have_http_status(:success) } + end + + context 'cancelable pipeline' do + let!(:running) { create(:ci_build, :running, pipeline: pipeline, stage: 'test', commands: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).to have_link('Cancel') } + it { expect(page).to have_selector('.ci-running') } + + context 'when canceling' do + before { click_link('Cancel') } + + it { expect(page).not_to have_link('Cancel') } + it { expect(page).to have_selector('.ci-canceled') } + end + end + + context 'retryable pipelines' do + let!(:failed) { create(:ci_build, :failed, pipeline: pipeline, stage: 'test', commands: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).to have_link('Retry') } + it { expect(page).to have_selector('.ci-failed') } + + context 'when retrying' do + before { click_link('Retry') } + + it { expect(page).not_to have_link('Retry') } + it { expect(page).to have_selector('.ci-pending') } + end + end + + context 'for generic statuses' do + context 'when running' do + let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it 'not be cancelable' do + expect(page).not_to have_link('Cancel') + end + + it 'pipeline is running' do + expect(page).to have_selector('.ci-running') + end + end + + context 'when failed' do + let!(:running) { create(:generic_commit_status, status: 'failed', pipeline: pipeline, stage: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it 'not be retryable' do + expect(page).not_to have_link('Retry') + end + + it 'pipeline is failed' do + expect(page).to have_selector('.ci-failed') + end + end + end + + context 'downloadable pipelines' do + context 'with artifacts' do + let!(:with_artifacts) { create(:ci_build, :artifacts, :success, pipeline: pipeline, name: 'rspec tests', stage: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).to have_selector('.build-artifacts') } + it { expect(page).to have_link(with_artifacts.name) } + end + + context 'without artifacts' do + let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') } + + it { expect(page).not_to have_selector('.build-artifacts') } + end + end + end + + describe 'GET /:project/pipelines/:id' do + let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') } + + before do + @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') + @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') + @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') + @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') + end + + before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } + + it 'showing a list of builds' do + expect(page).to have_content('Tests') + expect(page).to have_content(@success.id) + expect(page).to have_content('Deploy') + expect(page).to have_content(@failed.id) + expect(page).to have_content(@running.id) + expect(page).to have_content(@external.id) + expect(page).to have_content('Retry failed') + expect(page).to have_content('Cancel running') + end + + context 'retrying builds' do + it { expect(page).not_to have_content('retried') } + + context 'when retrying' do + before { click_on 'Retry failed' } + + it { expect(page).not_to have_content('Retry failed') } + it { expect(page).to have_content('retried') } + end + end + + context 'canceling builds' do + it { expect(page).not_to have_selector('.ci-canceled') } + + context 'when canceling' do + before { click_on 'Cancel running' } + + it { expect(page).not_to have_content('Cancel running') } + it { expect(page).to have_selector('.ci-canceled') } + end + end + end + + describe 'POST /:project/pipelines' do + let(:project) { create(:project) } + + before { visit new_namespace_project_pipeline_path(project.namespace, project) } + + context 'for valid commit' do + before { fill_in('Create for', with: 'master') } + + context 'with gitlab-ci.yml' do + before { stub_ci_pipeline_to_return_yaml_file } + + it { expect{ click_on 'Create pipeline' }.to change{ Ci::Pipeline.count }.by(1) } + end + + context 'without gitlab-ci.yml' do + before { click_on 'Create pipeline' } + + it { expect(page).to have_content('Missing .gitlab-ci.yml file') } + end + end + + context 'for invalid commit' do + before do + fill_in('Create for', with: 'invalid reference') + click_on 'Create pipeline' + end + + it { expect(page).to have_content('Reference not found') } + end + end +end diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb index 8f645438cff..787bf42d048 100644 --- a/spec/features/profiles/preferences_spec.rb +++ b/spec/features/profiles/preferences_spec.rb @@ -54,7 +54,7 @@ describe 'Profile > Preferences', feature: true do end end - describe 'User changes their default dashboard' do + describe 'User changes their default dashboard', js: true do it 'creates a flash message' do select 'Starred Projects', from: 'user_dashboard' click_button 'Save' @@ -66,8 +66,10 @@ describe 'Profile > Preferences', feature: true do select 'Starred Projects', from: 'user_dashboard' click_button 'Save' - click_link 'Dashboard' - expect(page.current_path).to eq starred_dashboard_projects_path + allowing_for_delay do + find('#logo').click + expect(page.current_path).to eq starred_dashboard_projects_path + end click_link 'Your Projects' expect(page.current_path).to eq dashboard_projects_path diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb index 13c9b95b316..51be81d634c 100644 --- a/spec/features/projects/badges/list_spec.rb +++ b/spec/features/projects/badges/list_spec.rb @@ -8,12 +8,10 @@ feature 'list of badges' do project = create(:project) project.team << [user, :master] login_as(user) - visit edit_namespace_project_path(project.namespace, project) + visit namespace_project_badges_path(project.namespace, project) end scenario 'user displays list of badges' do - click_link 'Badges' - expect(page).to have_content 'build status' expect(page).to have_content 'Markdown' expect(page).to have_content 'HTML' @@ -26,7 +24,6 @@ feature 'list of badges' do end scenario 'user changes current ref on badges list page', js: true do - click_link 'Badges' select2('improve/awesome', from: '#ref') expect(page).to have_content 'badges/improve/awesome/build.svg' diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb index 40ba0bdc115..15c381c0f5a 100644 --- a/spec/features/projects/commit/builds_spec.rb +++ b/spec/features/projects/commit/builds_spec.rb @@ -11,9 +11,9 @@ feature 'project commit builds' do context 'when no builds triggered yet' do background do - create(:ci_commit, project: project, - sha: project.commit.sha, - ref: 'master') + create(:ci_pipeline, project: project, + sha: project.commit.sha, + ref: 'master') end scenario 'user views commit builds page' do diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb new file mode 100644 index 00000000000..0c51fe72ca4 --- /dev/null +++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb @@ -0,0 +1,63 @@ +require 'rails_helper' + +feature 'Developer views empty project instructions', feature: true do + let(:project) { create(:empty_project, :empty_repo) } + let(:developer) { create(:user) } + + background do + project.team << [developer, :developer] + + login_as(developer) + end + + context 'without an SSH key' do + scenario 'defaults to HTTP' do + visit_project + + expect_instructions_for('http') + end + + scenario 'switches to SSH', js: true do + visit_project + + select_protocol('SSH') + + expect_instructions_for('ssh') + end + end + + context 'with an SSH key' do + background do + create(:personal_key, user: developer) + end + + scenario 'defaults to SSH' do + visit_project + + expect_instructions_for('ssh') + end + + scenario 'switches to HTTP', js: true do + visit_project + + select_protocol('HTTP') + + expect_instructions_for('http') + end + end + + def visit_project + visit namespace_project_path(project.namespace, project) + end + + def select_protocol(protocol) + find('#clone-dropdown').click + find(".#{protocol.downcase}-selector").click + end + + def expect_instructions_for(protocol) + msg = :"#{protocol.downcase}_url_to_repo" + + expect(page).to have_content("git clone #{project.send(msg)}") + end +end diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb new file mode 100644 index 00000000000..073a83b6896 --- /dev/null +++ b/spec/features/projects/files/gitignore_dropdown_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +feature 'User wants to add a .gitignore file', feature: true do + include WaitForAjax + + before do + user = create(:user) + project = create(:project) + project.team << [user, :master] + login_as user + visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: '.gitignore') + end + + scenario 'user can see .gitignore dropdown' do + expect(page).to have_css('.gitignore-selector') + end + + scenario 'user can pick a .gitignore file from the dropdown', js: true do + find('.js-gitignore-selector').click + wait_for_ajax + within '.gitignore-selector' do + find('.dropdown-input-field').set('rails') + find('.dropdown-content li', text: 'Rails').click + end + wait_for_ajax + + expect(page).to have_content('/.bundle') + expect(page).to have_content('# Gemfile.lock, .ruby-version, .ruby-gemset') + end +end diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index 3d6ffbc4c6b..ecc818eb1e1 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -25,7 +25,7 @@ feature 'project owner creates a license file', feature: true, js: true do file_content = find('.file-content') expect(file_content).to have_content('The MIT License (MIT)') - expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") fill_in :commit_message, with: 'Add a LICENSE file', visible: true click_button 'Commit Changes' @@ -33,7 +33,7 @@ feature 'project owner creates a license file', feature: true, js: true do expect(current_path).to eq( namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) expect(page).to have_content('The MIT License (MIT)') - expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end scenario 'project master creates a license file from the "Add license" link' do @@ -48,7 +48,7 @@ feature 'project owner creates a license file', feature: true, js: true do file_content = find('.file-content') expect(file_content).to have_content('The MIT License (MIT)') - expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") fill_in :commit_message, with: 'Add a LICENSE file', visible: true click_button 'Commit Changes' @@ -56,6 +56,6 @@ feature 'project owner creates a license file', feature: true, js: true do expect(current_path).to eq( namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) expect(page).to have_content('The MIT License (MIT)') - expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end end diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index 3268e240200..34eda29c285 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -24,7 +24,7 @@ feature 'project owner sees a link to create a license file in empty project', f file_content = find('.file-content') expect(file_content).to have_content('The MIT License (MIT)') - expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") fill_in :commit_message, with: 'Add a LICENSE file', visible: true # Remove pre-receive hook so we can push without auth @@ -34,6 +34,6 @@ feature 'project owner sees a link to create a license file in empty project', f expect(current_path).to eq( namespace_project_blob_path(project.namespace, project, 'master/LICENSE')) expect(page).to have_content('The MIT License (MIT)') - expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}") + expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}") end end diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb new file mode 100644 index 00000000000..461f1737928 --- /dev/null +++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +feature 'Issue prioritization', feature: true do + + let(:user) { create(:user) } + let(:project) { create(:project, name: 'test', namespace: user.namespace) } + + # Labels + let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) } + let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) } + let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) } + let(:label_4) { create(:label, title: 'label_4', project: project, priority: 4) } + let(:label_5) { create(:label, title: 'label_5', project: project) } # no priority + + # According to https://gitlab.com/gitlab-org/gitlab-ce/issues/14189#note_4360653 + context 'when issues have one label' do + scenario 'Are sorted properly' do + + # Issues + issue_1 = create(:issue, title: 'issue_1', project: project) + issue_2 = create(:issue, title: 'issue_2', project: project) + issue_3 = create(:issue, title: 'issue_3', project: project) + issue_4 = create(:issue, title: 'issue_4', project: project) + issue_5 = create(:issue, title: 'issue_5', project: project) + + # Assign labels to issues disorderly + issue_4.labels << label_1 + issue_3.labels << label_2 + issue_5.labels << label_3 + issue_2.labels << label_4 + issue_1.labels << label_5 + + login_as user + visit namespace_project_issues_path(project.namespace, project, sort: 'priority') + + # Ensure we are indicating that issues are sorted by priority + expect(page).to have_selector('.dropdown-toggle', text: 'Priority') + + page.within('.issues-holder') do + issue_titles = all('.issues-list .issue-title-text').map(&:text) + + expect(issue_titles).to eq(['issue_4', 'issue_3', 'issue_5', 'issue_2', 'issue_1']) + end + end + end + + context 'when issues have multiple labels' do + scenario 'Are sorted properly' do + + # Issues + issue_1 = create(:issue, title: 'issue_1', project: project) + issue_2 = create(:issue, title: 'issue_2', project: project) + issue_3 = create(:issue, title: 'issue_3', project: project) + issue_4 = create(:issue, title: 'issue_4', project: project) + issue_5 = create(:issue, title: 'issue_5', project: project) + issue_6 = create(:issue, title: 'issue_6', project: project) + issue_7 = create(:issue, title: 'issue_7', project: project) + issue_8 = create(:issue, title: 'issue_8', project: project) + + # Assign labels to issues disorderly + issue_5.labels << label_1 # 1 + issue_5.labels << label_2 + issue_8.labels << label_1 # 2 + issue_1.labels << label_2 # 3 + issue_1.labels << label_3 + issue_3.labels << label_2 # 4 + issue_3.labels << label_4 + issue_7.labels << label_2 # 5 + issue_2.labels << label_3 # 6 + issue_4.labels << label_4 # 7 + issue_6.labels << label_5 # 8 - No priority + + login_as user + visit namespace_project_issues_path(project.namespace, project, sort: 'priority') + + expect(page).to have_selector('.dropdown-toggle', text: 'Priority') + + page.within('.issues-holder') do + issue_titles = all('.issues-list .issue-title-text').map(&:text) + + expect(issue_titles[0..1]).to contain_exactly('issue_5', 'issue_8') + expect(issue_titles[2..4]).to contain_exactly('issue_1', 'issue_3', 'issue_7') + expect(issue_titles[5..-1]).to eq(['issue_2', 'issue_4', 'issue_6']) + end + end + end +end diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb new file mode 100644 index 00000000000..8550d279d09 --- /dev/null +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper' + +feature 'Prioritize labels', feature: true do + include WaitForAjax + + context 'when project belongs to user' do + let(:user) { create(:user) } + let(:project) { create(:project, name: 'test', namespace: user.namespace) } + + scenario 'user can prioritize a label', js: true do + bug = create(:label, title: 'bug') + wontfix = create(:label, title: 'wontfix') + + project.labels << bug + project.labels << wontfix + + login_as user + visit namespace_project_labels_path(project.namespace, project) + + expect(page).to have_content('No prioritized labels yet') + + page.within('.other-labels') do + first('.js-toggle-priority').click + wait_for_ajax + expect(page).not_to have_content('bug') + end + + page.within('.prioritized-labels') do + expect(page).not_to have_content('No prioritized labels yet') + expect(page).to have_content('bug') + end + end + + scenario 'user can unprioritize a label', js: true do + bug = create(:label, title: 'bug', priority: 1) + wontfix = create(:label, title: 'wontfix') + + project.labels << bug + project.labels << wontfix + + login_as user + visit namespace_project_labels_path(project.namespace, project) + + expect(page).to have_content('bug') + + page.within('.prioritized-labels') do + first('.js-toggle-priority').click + wait_for_ajax + expect(page).not_to have_content('bug') + end + + page.within('.other-labels') do + expect(page).to have_content('bug') + expect(page).to have_content('wontfix') + end + end + + scenario 'user can sort prioritized labels and persist across reloads', js: true do + bug = create(:label, title: 'bug', priority: 1) + wontfix = create(:label, title: 'wontfix', priority: 2) + + project.labels << bug + project.labels << wontfix + + login_as user + visit namespace_project_labels_path(project.namespace, project) + + expect(page).to have_content 'bug' + expect(page).to have_content 'wontfix' + + # Sort labels + find("#label_#{bug.id}").drag_to find("#label_#{wontfix.id}") + + page.within('.prioritized-labels') do + expect(first('li')).to have_content('wontfix') + expect(page.all('li').last).to have_content('bug') + end + + visit current_url + + page.within('.prioritized-labels') do + expect(first('li')).to have_content('wontfix') + expect(page.all('li').last).to have_content('bug') + end + end + end + + context 'as a guest' do + it 'can not prioritize labels' do + user = create(:user) + guest = create(:user) + project = create(:project, name: 'test', namespace: user.namespace) + + create(:label, title: 'bug') + + login_as guest + visit namespace_project_labels_path(project.namespace, project) + + expect(page).not_to have_css('.prioritized-labels') + end + end + + context 'as a non signed in user' do + it 'can not prioritize labels' do + user = create(:user) + project = create(:project, name: 'test', namespace: user.namespace) + + create(:label, title: 'bug') + + visit namespace_project_labels_path(project.namespace, project) + + expect(page).not_to have_css('.prioritized-labels') + end + end +end diff --git a/spec/features/project/shortcuts_spec.rb b/spec/features/projects/shortcuts_spec.rb index 2595c4181e5..54aa9c66a08 100644 --- a/spec/features/project/shortcuts_spec.rb +++ b/spec/features/projects/shortcuts_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' feature 'Project shortcuts', feature: true do - let(:project) { create(:project) } + let(:project) { create(:project, name: 'Victorialand') } let(:user) { create(:user) } describe 'On a project', js: true do @@ -14,7 +14,7 @@ feature 'Project shortcuts', feature: true do describe 'pressing "i"' do it 'redirects to new issue page' do find('body').native.send_key('i') - expect(page).to have_content('New Issue') + expect(page).to have_content('Victorialand') end end end diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index 8edeb8d18af..a5ed3595b0a 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -29,8 +29,8 @@ describe "Runners" do end before do - expect(page).to_not have_content(@specific_runner3.display_name) - expect(page).to_not have_content(@specific_runner3.display_name) + expect(page).not_to have_content(@specific_runner3.display_name) + expect(page).not_to have_content(@specific_runner3.display_name) end it "places runners in right places" do @@ -110,4 +110,37 @@ describe "Runners" do expect(page).to have_content(@specific_runner.platform) end end + + feature 'configuring runners ability to picking untagged jobs' do + given(:project) { create(:empty_project) } + given(:runner) { create(:ci_runner) } + + background do + project.team << [user, :master] + project.runners << runner + end + + scenario 'user checks default configuration' do + visit namespace_project_runner_path(project.namespace, project, runner) + + expect(page).to have_content 'Can run untagged jobs Yes' + end + + context 'when runner has tags' do + before { runner.update_attribute(:tag_list, ['tag']) } + + scenario 'user wants to prevent runner from running untagged job' do + visit runners_path(project) + page.within('.activated-specific-runners') do + first('small > a').click + end + + uncheck 'runner_run_untagged' + click_button 'Save changes' + + expect(page).to have_content 'Can run untagged jobs No' + expect(runner.reload.run_untagged?).to eq false + end + end + end end diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 4def4f99bc0..c5f741709ad 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -142,8 +142,8 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/builds/:id" do - let(:commit) { create(:ci_commit, project: project) } - let(:build) { create(:ci_build, commit: commit) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } subject { namespace_project_build_path(project.namespace, project, build.id) } context "when allowed for public" do diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index 58aabd913eb..4229e82b443 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -2,20 +2,45 @@ require 'spec_helper' feature 'Signup', feature: true do describe 'signup with no errors' do - it 'creates the user account and sends a confirmation email' do - user = build(:user) - visit root_path + context "when sending confirmation email" do + before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) } - fill_in 'new_user_name', with: user.name - fill_in 'new_user_username', with: user.username - fill_in 'new_user_email', with: user.email - fill_in 'new_user_password', with: user.password - click_button "Sign up" + it 'creates the user account and sends a confirmation email' do + user = build(:user) + + visit root_path + + fill_in 'new_user_name', with: user.name + fill_in 'new_user_username', with: user.username + fill_in 'new_user_email', with: user.email + fill_in 'new_user_password', with: user.password + click_button "Sign up" - expect(current_path).to eq users_almost_there_path - expect(page).to have_content("Please check your email to confirm your account") + expect(current_path).to eq users_almost_there_path + expect(page).to have_content("Please check your email to confirm your account") + end end + + context "when not sending confirmation email" do + before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) } + + it 'creates the user account and goes to dashboard' do + user = build(:user) + + visit root_path + + fill_in 'new_user_name', with: user.name + fill_in 'new_user_username', with: user.username + fill_in 'new_user_email', with: user.email + fill_in 'new_user_password', with: user.password + click_button "Sign up" + + expect(current_path).to eq dashboard_projects_path + expect(page).to have_content("Welcome! You have signed up successfully.") + end + end + end describe 'signup with errors' do diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb new file mode 100644 index 00000000000..08a97085a9c --- /dev/null +++ b/spec/features/tags/master_creates_tag_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +feature 'Master creates tag', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + + before do + project.team << [user, :master] + login_with(user) + visit namespace_project_tags_path(project.namespace, project) + end + + scenario 'with an invalid name displays an error' do + create_tag_in_form(tag: 'v 1.0', ref: 'master') + + expect(page).to have_content 'Tag name invalid' + end + + scenario 'with an invalid reference displays an error' do + create_tag_in_form(tag: 'v2.0', ref: 'foo') + + expect(page).to have_content 'Target foo is invalid' + end + + scenario 'that already exists displays an error' do + create_tag_in_form(tag: 'v1.1.0', ref: 'master') + + expect(page).to have_content 'Tag v1.1.0 already exists' + end + + scenario 'with multiline message displays the message in a <pre> block' do + create_tag_in_form(tag: 'v3.0', ref: 'master', message: "Awesome tag message\n\n- hello\n- world") + + expect(current_path).to eq( + namespace_project_tag_path(project.namespace, project, 'v3.0')) + expect(page).to have_content 'v3.0' + page.within 'pre.body' do + expect(page).to have_content "Awesome tag message\n\n- hello\n- world" + end + end + + scenario 'with multiline release notes parses the release note as Markdown' do + create_tag_in_form(tag: 'v4.0', ref: 'master', desc: "Awesome release notes\n\n- hello\n- world") + + expect(current_path).to eq( + namespace_project_tag_path(project.namespace, project, 'v4.0')) + expect(page).to have_content 'v4.0' + page.within '.description' do + expect(page).to have_content 'Awesome release notes' + expect(page).to have_selector('ul li', count: 2) + end + end + + def create_tag_in_form(tag:, ref:, message: nil, desc: nil) + click_link 'New tag' + fill_in 'tag_name', with: tag + fill_in 'ref', with: ref + fill_in 'message', with: message unless message.nil? + fill_in 'release_description', with: desc unless desc.nil? + click_button 'Create tag' + end +end diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb new file mode 100644 index 00000000000..f0990118e3c --- /dev/null +++ b/spec/features/tags/master_deletes_tag_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +feature 'Master deletes tag', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + + before do + project.team << [user, :master] + login_with(user) + visit namespace_project_tags_path(project.namespace, project) + end + + context 'from the tags list page' do + scenario 'deletes the tag' do + expect(page).to have_content 'v1.1.0' + + page.within('.content') do + first('.btn-remove').click + end + + expect(current_path).to eq( + namespace_project_tags_path(project.namespace, project)) + expect(page).not_to have_content 'v1.1.0' + end + + end + + context 'from a specific tag page' do + scenario 'deletes the tag' do + click_on 'v1.0.0' + expect(current_path).to eq( + namespace_project_tag_path(project.namespace, project, 'v1.0.0')) + + click_on 'Delete tag' + + expect(current_path).to eq( + namespace_project_tags_path(project.namespace, project)) + expect(page).not_to have_content 'v1.0.0' + end + end +end diff --git a/spec/features/tags/master_updates_tag_spec.rb b/spec/features/tags/master_updates_tag_spec.rb new file mode 100644 index 00000000000..6b5b3122f72 --- /dev/null +++ b/spec/features/tags/master_updates_tag_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +feature 'Master updates tag', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + + before do + project.team << [user, :master] + login_with(user) + visit namespace_project_tags_path(project.namespace, project) + end + + context 'from the tags list page' do + scenario 'updates the release notes' do + page.within(first('.content-list .controls')) do + click_link 'Edit release notes' + end + + fill_in 'release_description', with: 'Awesome release notes' + click_button 'Save changes' + + expect(current_path).to eq( + namespace_project_tag_path(project.namespace, project, 'v1.1.0')) + expect(page).to have_content 'v1.1.0' + expect(page).to have_content 'Awesome release notes' + end + end + + context 'from a specific tag page' do + scenario 'updates the release notes' do + click_on 'v1.1.0' + click_link 'Edit release notes' + fill_in 'release_description', with: 'Awesome release notes' + click_button 'Save changes' + + expect(current_path).to eq( + namespace_project_tag_path(project.namespace, project, 'v1.1.0')) + expect(page).to have_content 'v1.1.0' + expect(page).to have_content 'Awesome release notes' + end + end +end diff --git a/spec/features/tags/master_views_tags_spec.rb b/spec/features/tags/master_views_tags_spec.rb new file mode 100644 index 00000000000..29d2c244720 --- /dev/null +++ b/spec/features/tags/master_views_tags_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +feature 'Master views tags', feature: true do + let(:user) { create(:user) } + + before do + project.team << [user, :master] + login_with(user) + end + + context 'when project has no tags' do + let(:project) { create(:project_empty_repo) } + before do + visit namespace_project_path(project.namespace, project) + click_on 'README' + fill_in :commit_message, with: 'Add a README file', visible: true + # Remove pre-receive hook so we can push without auth + FileUtils.rm_f(File.join(project.repository.path, 'hooks', 'pre-receive')) + click_button 'Commit Changes' + visit namespace_project_tags_path(project.namespace, project) + end + + scenario 'displays a specific message' do + expect(page).to have_content 'Repository has no tags yet.' + end + end + + context 'when project has tags' do + let(:project) { create(:project, namespace: user.namespace) } + before do + visit namespace_project_tags_path(project.namespace, project) + end + + scenario 'views the tags list page' do + expect(page).to have_content 'v1.0.0' + end + + scenario 'views a specific tag page' do + click_on 'v1.0.0' + + expect(current_path).to eq( + namespace_project_tag_path(project.namespace, project, 'v1.0.0')) + expect(page).to have_content 'v1.0.0' + expect(page).to have_content 'This tag has no release notes.' + end + + describe 'links on the tag page' do + scenario 'has a button to browse files' do + click_on 'v1.0.0' + + expect(current_path).to eq( + namespace_project_tag_path(project.namespace, project, 'v1.0.0')) + + click_on 'Browse files' + + expect(current_path).to eq( + namespace_project_tree_path(project.namespace, project, 'v1.0.0')) + end + + scenario 'has a button to browse commits' do + click_on 'v1.0.0' + + expect(current_path).to eq( + namespace_project_tag_path(project.namespace, project, 'v1.0.0')) + + click_on 'Browse commits' + + expect(current_path).to eq( + namespace_project_commits_path(project.namespace, project, 'v1.0.0')) + end + end + end +end diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index b7368cca29d..6ed279ef9be 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -75,7 +75,10 @@ feature 'Task Lists', feature: true do describe 'for Notes' do let!(:issue) { create(:issue, author: user, project: project) } - let!(:note) { create(:note, note: markdown, noteable: issue, author: user) } + let!(:note) do + create(:note, note: markdown, noteable: issue, + project: project, author: user) + end it 'renders for note body' do visit_issue(project, issue) diff --git a/spec/features/todos/target_state_spec.rb b/spec/features/todos/target_state_spec.rb new file mode 100644 index 00000000000..32fa88a2b21 --- /dev/null +++ b/spec/features/todos/target_state_spec.rb @@ -0,0 +1,65 @@ +require 'rails_helper' + +feature 'Todo target states', feature: true do + let(:user) { create(:user) } + let(:author) { create(:user) } + let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + + before do + login_as user + end + + scenario 'on a closed issue todo has closed label' do + issue_closed = create(:issue, state: 'closed') + create_todo issue_closed + visit dashboard_todos_path + + page.within '.todos-list' do + expect(page).to have_content('Closed') + end + end + + scenario 'on an open issue todo does not have an open label' do + issue_open = create(:issue) + create_todo issue_open + visit dashboard_todos_path + + page.within '.todos-list' do + expect(page).not_to have_content('Open') + end + end + + scenario 'on a merged merge request todo has merged label' do + mr_merged = create(:merge_request, :simple, author: user, state: 'merged') + create_todo mr_merged + visit dashboard_todos_path + + page.within '.todos-list' do + expect(page).to have_content('Merged') + end + end + + scenario 'on a closed merge request todo has closed label' do + mr_closed = create(:merge_request, :simple, author: user, state: 'closed') + create_todo mr_closed + visit dashboard_todos_path + + page.within '.todos-list' do + expect(page).to have_content('Closed') + end + end + + scenario 'on an open merge request todo does not have an open label' do + mr_open = create(:merge_request, :simple, author: user) + create_todo mr_open + visit dashboard_todos_path + + page.within '.todos-list' do + expect(page).not_to have_content('Open') + end + end + + def create_todo(target) + create(:todo, :mentioned, user: user, project: project, target: target, author: author) + end +end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 3354f529295..8e1833a069e 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Dashboard Todos', feature: true do let(:user) { create(:user) } let(:author) { create(:user) } - let(:project) { create(:project) } + let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } let(:issue) { create(:issue) } describe 'GET /dashboard/todos' do @@ -43,6 +43,27 @@ describe 'Dashboard Todos', feature: true do end end + context 'User has Todos with labels spanning multiple projects' do + before do + label1 = create(:label, project: project) + note1 = create(:note_on_issue, note: "Hello #{label1.to_reference(format: :name)}", noteable_id: issue.id, noteable_type: 'Issue', project: issue.project) + create(:todo, :mentioned, project: project, target: issue, user: user, note_id: note1.id) + + project2 = create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) + label2 = create(:label, project: project2) + issue2 = create(:issue, project: project2) + note2 = create(:note_on_issue, note: "Test #{label2.to_reference(format: :name)}", noteable_id: issue2.id, noteable_type: 'Issue', project: project2) + create(:todo, :mentioned, project: project2, target: issue2, user: user, note_id: note2.id) + + login_as(user) + visit dashboard_todos_path + end + + it 'shows page with two Todos' do + expect(page).to have_selector('.todos-list .todo', count: 2) + end + end + context 'User has multiple pages of Todos' do before do allow(Todo).to receive(:default_per_page).and_return(1) @@ -77,5 +98,18 @@ describe 'Dashboard Todos', feature: true do end end end + + context 'User has a Todo in a project pending deletion' do + before do + deleted_project = create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC, pending_delete: true) + create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author) + login_as(user) + visit dashboard_todos_path + end + + it 'shows "All done" message' do + expect(page).to have_content "You're all done!" + end + end end end diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb new file mode 100644 index 00000000000..366a90228b1 --- /dev/null +++ b/spec/features/u2f_spec.rb @@ -0,0 +1,239 @@ +require 'spec_helper' + +feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: true, js: true do + def register_u2f_device(u2f_device = nil) + u2f_device ||= FakeU2fDevice.new(page) + u2f_device.respond_to_u2f_registration + click_on 'Setup New U2F Device' + expect(page).to have_content('Your device was successfully set up') + click_on 'Register U2F Device' + u2f_device + end + + describe "registration" do + let(:user) { create(:user) } + before { login_as(user) } + + describe 'when 2FA via OTP is disabled' do + it 'allows registering a new device' do + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + + register_u2f_device + + expect(page.body).to match('Your U2F device was registered') + end + + it 'allows registering more than one device' do + visit profile_account_path + + # First device + click_on 'Enable Two-Factor Authentication' + register_u2f_device + expect(page.body).to match('Your U2F device was registered') + + # Second device + click_on 'Manage Two-Factor Authentication' + register_u2f_device + expect(page.body).to match('Your U2F device was registered') + click_on 'Manage Two-Factor Authentication' + + expect(page.body).to match('You have 2 U2F devices registered') + end + end + + describe 'when 2FA via OTP is enabled' do + before { user.update_attributes(otp_required_for_login: true) } + + it 'allows registering a new device' do + visit profile_account_path + click_on 'Manage Two-Factor Authentication' + expect(page.body).to match("You've already enabled two-factor authentication using mobile") + + register_u2f_device + + expect(page.body).to match('Your U2F device was registered') + end + + it 'allows registering more than one device' do + visit profile_account_path + + # First device + click_on 'Manage Two-Factor Authentication' + register_u2f_device + expect(page.body).to match('Your U2F device was registered') + + # Second device + click_on 'Manage Two-Factor Authentication' + register_u2f_device + expect(page.body).to match('Your U2F device was registered') + + click_on 'Manage Two-Factor Authentication' + expect(page.body).to match('You have 2 U2F devices registered') + end + end + + it 'allows the same device to be registered for multiple users' do + # First user + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + u2f_device = register_u2f_device + expect(page.body).to match('Your U2F device was registered') + logout + + # Second user + login_as(:user) + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + register_u2f_device(u2f_device) + expect(page.body).to match('Your U2F device was registered') + + expect(U2fRegistration.count).to eq(2) + end + + context "when there are form errors" do + it "doesn't register the device if there are errors" do + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + + # Have the "u2f device" respond with bad data + page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };") + click_on 'Setup New U2F Device' + expect(page).to have_content('Your device was successfully set up') + click_on 'Register U2F Device' + + expect(U2fRegistration.count).to eq(0) + expect(page.body).to match("The form contains the following error") + expect(page.body).to match("did not send a valid JSON response") + end + + it "allows retrying registration" do + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + + # Failed registration + page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };") + click_on 'Setup New U2F Device' + expect(page).to have_content('Your device was successfully set up') + click_on 'Register U2F Device' + expect(page.body).to match("The form contains the following error") + + # Successful registration + register_u2f_device + + expect(page.body).to match('Your U2F device was registered') + expect(U2fRegistration.count).to eq(1) + end + end + end + + describe "authentication" do + let(:user) { create(:user) } + + before do + # Register and logout + login_as(user) + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + @u2f_device = register_u2f_device + logout + end + + describe "when 2FA via OTP is disabled" do + it "allows logging in with the U2F device" do + login_with(user) + + @u2f_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + click_on "Authenticate via U2F Device" + + expect(page.body).to match('Signed in successfully') + end + end + + describe "when 2FA via OTP is enabled" do + it "allows logging in with the U2F device" do + user.update_attributes(otp_required_for_login: true) + login_with(user) + + @u2f_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + click_on "Authenticate via U2F Device" + + expect(page.body).to match('Signed in successfully') + end + end + + describe "when a given U2F device has already been registered by another user" do + describe "but not the current user" do + it "does not allow logging in with that particular device" do + # Register current user with the different U2F device + current_user = login_as(:user) + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + register_u2f_device + logout + + # Try authenticating user with the old U2F device + login_as(current_user) + @u2f_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + click_on "Authenticate via U2F Device" + + expect(page.body).to match('Authentication via U2F device failed') + end + end + + describe "and also the current user" do + it "allows logging in with that particular device" do + # Register current user with the same U2F device + current_user = login_as(:user) + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + register_u2f_device(@u2f_device) + logout + + # Try authenticating user with the same U2F device + login_as(current_user) + @u2f_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + click_on "Authenticate via U2F Device" + + expect(page.body).to match('Signed in successfully') + end + end + end + + describe "when a given U2F device has not been registered" do + it "does not allow logging in with that particular device" do + unregistered_device = FakeU2fDevice.new(page) + login_as(user) + unregistered_device.respond_to_u2f_authentication + click_on "Login Via U2F Device" + expect(page.body).to match('We heard back from your U2F device') + click_on "Authenticate via U2F Device" + + expect(page.body).to match('Authentication via U2F device failed') + end + end + end + + describe "when two-factor authentication is disabled" do + let(:user) { create(:user) } + + before do + login_as(user) + visit profile_account_path + click_on 'Enable Two-Factor Authentication' + register_u2f_device + end + + it "deletes u2f registrations" do + expect { click_on "Disable" }.to change { U2fRegistration.count }.from(1).to(0) + end + end +end diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb index afea1840cd7..a2b8f7b6931 100644 --- a/spec/features/variables_spec.rb +++ b/spec/features/variables_spec.rb @@ -1,24 +1,53 @@ require 'spec_helper' -describe "Variables" do - let(:user) { create(:user) } - before { login_as(user) } - - describe "specific runners" do - before do - @project = FactoryGirl.create :empty_project - @project.team << [user, :master] +describe 'Project variables', js: true do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:variable) { create(:ci_variable, key: 'test') } + + before do + login_as(user) + project.team << [user, :master] + project.variables << variable + + visit namespace_project_variables_path(project.namespace, project) + end + + it 'should show list of variables' do + page.within('.variables-table') do + expect(page).to have_content(variable.key) + end + end + + it 'should add new variable' do + fill_in('variable_key', with: 'key') + fill_in('variable_value', with: 'key value') + click_button('Add new variable') + + page.within('.variables-table') do + expect(page).to have_content('key') + end + end + + it 'should delete variable' do + page.within('.variables-table') do + find('.btn-variable-delete').click + end + + expect(page).not_to have_selector('variables-table') + end + + it 'should edit variable' do + page.within('.variables-table') do + find('.btn-variable-edit').click end - it "creates variable", js: true do - visit namespace_project_variables_path(@project.namespace, @project) - click_on "Add a variable" - fill_in "Key", with: "SECRET_KEY" - fill_in "Value", with: "SECRET_VALUE" - click_on "Save changes" + fill_in('variable_key', with: 'key') + fill_in('variable_value', with: 'key value') + click_button('Save variable') - expect(page).to have_content("Variables were successfully updated.") - expect(@project.variables.count).to eq(1) + page.within('.variables-table') do + expect(page).to have_content('key') end end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index bc607a29751..ec8809e6926 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' describe IssuesFinder do - let(:user) { create :user } - let(:user2) { create :user } - let(:project1) { create(:project) } - let(:project2) { create(:project) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:project1) { create(:empty_project) } + let(:project2) { create(:empty_project) } let(:milestone) { create(:milestone, project: project1) } let(:label) { create(:label, project: project2) } let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone) } @@ -16,101 +16,147 @@ describe IssuesFinder do project1.team << [user, :master] project2.team << [user, :developer] project2.team << [user2, :developer] + + issue1 + issue2 + issue3 end - describe :execute do - before :each do - issue1 - issue2 - issue3 - end + describe '#execute' do + let(:search_user) { user } + let(:params) { {} } + let(:issues) { IssuesFinder.new(search_user, params.merge(scope: scope, state: 'opened')).execute } context 'scope: all' do - it 'should filter by all' do - params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues.size).to eq(3) + let(:scope) { 'all' } + + it 'returns all issues' do + expect(issues).to contain_exactly(issue1, issue2, issue3) end - it 'should filter by assignee id' do - params = { scope: "all", assignee_id: user.id, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues.size).to eq(2) + context 'filtering by assignee ID' do + let(:params) { { assignee_id: user.id } } + + it 'returns issues assigned to that user' do + expect(issues).to contain_exactly(issue1, issue2) + end end - it 'should filter by author id' do - params = { scope: "all", author_id: user2.id, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues).to eq([issue3]) + context 'filtering by author ID' do + let(:params) { { author_id: user2.id } } + + it 'returns issues created by that user' do + expect(issues).to contain_exactly(issue3) + end end - it 'should filter by milestone id' do - params = { scope: "all", milestone_title: milestone.title, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues).to eq([issue1]) + context 'filtering by milestone' do + let(:params) { { milestone_title: milestone.title } } + + it 'returns issues assigned to that milestone' do + expect(issues).to contain_exactly(issue1) + end end - it 'should filter by no milestone id' do - params = { scope: "all", milestone_title: Milestone::None.title, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues).to match_array([issue2, issue3]) + context 'filtering by no milestone' do + let(:params) { { milestone_title: Milestone::None.title } } + + it 'returns issues with no milestone' do + expect(issues).to contain_exactly(issue2, issue3) + end end - it 'should filter by label name' do - params = { scope: "all", label_name: label.title, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues).to eq([issue2]) + context 'filtering by upcoming milestone' do + let(:params) { { milestone_title: Milestone::Upcoming.name } } + + let(:project_no_upcoming_milestones) { create(:empty_project, :public) } + let(:project_next_1_1) { create(:empty_project, :public) } + let(:project_next_8_8) { create(:empty_project, :public) } + + let(:yesterday) { Date.today - 1.day } + let(:tomorrow) { Date.today + 1.day } + let(:two_days_from_now) { Date.today + 2.days } + let(:ten_days_from_now) { Date.today + 10.days } + + let(:milestones) do + [ + create(:milestone, :closed, project: project_no_upcoming_milestones), + create(:milestone, project: project_next_1_1, title: '1.1', due_date: two_days_from_now), + create(:milestone, project: project_next_1_1, title: '8.8', due_date: ten_days_from_now), + create(:milestone, project: project_next_8_8, title: '1.1', due_date: yesterday), + create(:milestone, project: project_next_8_8, title: '8.8', due_date: tomorrow) + ] + end + + before do + milestones.each do |milestone| + create(:issue, project: milestone.project, milestone: milestone, author: user, assignee: user) + end + end + + it 'returns issues in the upcoming milestone for each project' do + expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.1', '8.8') + expect(issues.map { |issue| issue.milestone.due_date }).to contain_exactly(tomorrow, two_days_from_now) + end end - it 'returns unique issues when filtering by multiple labels' do - label2 = create(:label, project: project2) + context 'filtering by label' do + let(:params) { { label_name: label.title } } - create(:label_link, label: label2, target: issue2) + it 'returns issues with that label' do + expect(issues).to contain_exactly(issue2) + end + end - params = { - scope: 'all', - label_name: [label.title, label2.title].join(','), - state: 'opened' - } + context 'filtering by multiple labels' do + let(:params) { { label_name: [label.title, label2.title].join(',') } } + let(:label2) { create(:label, project: project2) } - issues = IssuesFinder.new(user, params).execute + before { create(:label_link, label: label2, target: issue2) } - expect(issues).to eq([issue2]) + it 'returns the unique issues with any of those labels' do + expect(issues).to contain_exactly(issue2) + end end - it 'should filter by no label name' do - params = { scope: "all", label_name: Label::None.title, state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues).to match_array([issue1, issue3]) + context 'filtering by no label' do + let(:params) { { label_name: Label::None.title } } + + it 'returns issues with no labels' do + expect(issues).to contain_exactly(issue1, issue3) + end end - it 'should be empty for unauthorized user' do - params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new(nil, params).execute - expect(issues.size).to be_zero + context 'when the user is unauthorized' do + let(:search_user) { nil } + + it 'returns no results' do + expect(issues).to be_empty + end end - it 'should not include unauthorized issues' do - params = { scope: "all", state: 'opened' } - issues = IssuesFinder.new(user2, params).execute - expect(issues.size).to eq(2) - expect(issues).not_to include(issue1) - expect(issues).to include(issue2) - expect(issues).to include(issue3) + context 'when the user can see some, but not all, issues' do + let(:search_user) { user2 } + + it 'returns only issues they can see' do + expect(issues).to contain_exactly(issue2, issue3) + end end end context 'personal scope' do - it 'should filter by assignee' do - params = { scope: "assigned-to-me", state: 'opened' } - issues = IssuesFinder.new(user, params).execute - expect(issues.size).to eq(2) + let(:scope) { 'assigned-to-me' } + + it 'returns issue assigned to the user' do + expect(issues).to contain_exactly(issue1, issue2) end - it 'should filter by project' do - params = { scope: "assigned-to-me", state: 'opened', project_id: project1.id } - issues = IssuesFinder.new(user, params).execute - expect(issues.size).to eq(1) + context 'filtering by project' do + let(:params) { { project_id: project1.id } } + + it 'returns issues assigned to the user in that project' do + expect(issues).to contain_exactly(issue1) + end end end end diff --git a/spec/fixtures/container_registry/config_blob.json b/spec/fixtures/container_registry/config_blob.json new file mode 100644 index 00000000000..1028c994a24 --- /dev/null +++ b/spec/fixtures/container_registry/config_blob.json @@ -0,0 +1 @@ +{"architecture":"amd64","config":{"Hostname":"b14cd8298755","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":null,"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"b14cd82987550b01af9a666a2f4c996280a6152e66873134fae5a0f223dc5976","container_config":{"Hostname":"b14cd8298755","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["/bin/sh","-c","#(nop) ADD file:033ab063740d9ff4dcfb1c69eccf25f91d88729f57cd5a73050e014e3e094aa0 in /"],"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"created":"2016-04-01T20:53:00.160300546Z","docker_version":"1.9.1","history":[{"created":"2016-04-01T20:53:00.160300546Z","created_by":"/bin/sh -c #(nop) ADD file:033ab063740d9ff4dcfb1c69eccf25f91d88729f57cd5a73050e014e3e094aa0 in /"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:c56b7dabbc7aa730eeab07668bdcbd7e3d40855047ca9a0cc1bfed23a2486111"]}} diff --git a/spec/fixtures/container_registry/tag_manifest.json b/spec/fixtures/container_registry/tag_manifest.json new file mode 100644 index 00000000000..1b6008e2872 --- /dev/null +++ b/spec/fixtures/container_registry/tag_manifest.json @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/octet-stream","size":1145,"digest":"sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":2319870,"digest":"sha256:420890c9e918b6668faaedd9000e220190f2493b0693ee563ebd7b4cc754a57d"}]} diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 1772cc3f6a4..c75d28d9801 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -136,7 +136,7 @@ But it shouldn't autolink text inside certain tags: ### ExternalLinkFilter -External links get a `rel="nofollow"` attribute: +External links get a `rel="nofollow noreferrer"` and `target="_blank"` attributes: - [Google](https://google.com/) - [GitLab Root](<%= Gitlab.config.gitlab.url %>) @@ -216,10 +216,14 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e #### MilestoneReferenceFilter -- Milestone: <%= milestone.to_reference %> +- Milestone by ID: <%= simple_milestone.to_reference %> +- Milestone by name: <%= Milestone.reference_prefix %><%= simple_milestone.name %> +- Milestone by name in quotes: <%= milestone.to_reference(format: :name) %> - Milestone in another project: <%= xmilestone.to_reference(project) %> -- Ignored in code: `<%= milestone.to_reference %>` -- Link to milestone by URL: [Milestone](<%= urls.namespace_project_milestone_url(milestone.project.namespace, milestone.project, milestone) %>) +- Ignored in code: `<%= simple_milestone.to_reference %>` +- Ignored in links: [Link to <%= simple_milestone.to_reference %>](#milestone-link) +- Milestone by URL: <%= urls.namespace_project_milestone_url(milestone.project.namespace, milestone.project, milestone) %> +- Link to milestone by URL: [Milestone](<%= milestone.to_reference %>) ### Task Lists @@ -239,3 +243,16 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - [[link-text|http://example.com/pdfs/gollum.pdf]] - [[images/example.jpg]] - [[http://example.com/images/example.jpg]] + +### Inline Diffs + +With inline diffs tags you can display {+ additions +} or [- deletions -]. + +The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}. + +However the wrapping tags can not be mixed as such - + +- {+ additions +] +- [+ additions +} +- {- delletions -] +- [- delletions -} diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index e47a54fdac5..49ea4fa6d3e 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" describe AuthHelper do describe "button_based_providers" do - it 'returns all enabled providers' do + it 'returns all enabled providers from devise' do allow(helper).to receive(:auth_providers) { [:twitter, :github] } expect(helper.button_based_providers).to include(*[:twitter, :github]) end @@ -17,4 +17,49 @@ describe AuthHelper do expect(helper.button_based_providers).to eq([]) end end + + describe 'enabled_button_based_providers' do + before do + allow(helper).to receive(:auth_providers) { [:twitter, :github] } + end + + context 'all providers are enabled to sign in' do + it 'returns all the enabled providers from settings' do + expect(helper.enabled_button_based_providers).to include('twitter', 'github') + end + end + + context 'GitHub OAuth sign in is disabled from application setting' do + it "doesn't return github as provider" do + stub_application_setting( + disabled_oauth_sign_in_sources: ['github'] + ) + + expect(helper.enabled_button_based_providers).to include('twitter') + expect(helper.enabled_button_based_providers).not_to include('github') + end + end + end + + describe 'button_based_providers_enabled?' do + before do + allow(helper).to receive(:auth_providers) { [:twitter, :github] } + end + + context 'button based providers enabled' do + it 'returns true' do + expect(helper.button_based_providers_enabled?).to be true + end + end + + context 'all the button based providers are disabled via application_setting' do + it 'returns false' do + stub_application_setting( + disabled_oauth_sign_in_sources: ['github', 'twitter'] + ) + + expect(helper.button_based_providers_enabled?).to be false + end + end + end end diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb index f942695b6f0..45199d0f09d 100644 --- a/spec/helpers/ci_status_helper_spec.rb +++ b/spec/helpers/ci_status_helper_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe CiStatusHelper do include IconsHelper - let(:success_commit) { double("Ci::Commit", status: 'success') } - let(:failed_commit) { double("Ci::Commit", status: 'failed') } + let(:success_commit) { double("Ci::Pipeline", status: 'success') } + let(:failed_commit) { double("Ci::Pipeline", status: 'failed') } describe 'ci_icon_for_status' do it { expect(helper.ci_icon_for_status(success_commit.status)).to include('fa-check') } diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index b7810185d16..52764f41e0d 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -93,9 +93,9 @@ describe DiffHelper do it "returns strings with marked inline diffs" do marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line) - expect(marked_old_line).to eq("abc <span class='idiff left right'>'def'</span>") + expect(marked_old_line).to eq("abc <span class='idiff left right deletion'>'def'</span>") expect(marked_old_line).to be_html_safe - expect(marked_new_line).to eq("abc <span class='idiff left right'>"def"</span>") + expect(marked_new_line).to eq("abc <span class='idiff left right addition'>"def"</span>") expect(marked_new_line).to be_html_safe end end diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index e68a5ec29ab..c0d2be98e85 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -1,64 +1,65 @@ require 'spec_helper' describe EventsHelper do - include ApplicationHelper - include GitlabMarkdownHelper + describe '#event_note' do + before do + allow(helper).to receive(:current_user).and_return(double) + end - let(:current_user) { create(:user, email: "current@email.com") } + it 'should display one line of plain text without alteration' do + input = 'A short, plain note' + expect(helper.event_note(input)).to match(input) + expect(helper.event_note(input)).not_to match(/\.\.\.\z/) + end - it 'should display one line of plain text without alteration' do - input = 'A short, plain note' - expect(event_note(input)).to match(input) - expect(event_note(input)).not_to match(/\.\.\.\z/) - end + it 'should display inline code' do + input = 'A note with `inline code`' + expected = 'A note with <code>inline code</code>' - it 'should display inline code' do - input = 'A note with `inline code`' - expected = 'A note with <code>inline code</code>' + expect(helper.event_note(input)).to match(expected) + end - expect(event_note(input)).to match(expected) - end + it 'should truncate a note with multiple paragraphs' do + input = "Paragraph 1\n\nParagraph 2" + expected = 'Paragraph 1...' - it 'should truncate a note with multiple paragraphs' do - input = "Paragraph 1\n\nParagraph 2" - expected = 'Paragraph 1...' + expect(helper.event_note(input)).to match(expected) + end - expect(event_note(input)).to match(expected) - end + it 'should display the first line of a code block' do + input = "```\nCode block\nwith two lines\n```" + expected = %r{<pre.+><code>Code block\.\.\.</code></pre>} - it 'should display the first line of a code block' do - input = "```\nCode block\nwith two lines\n```" - expected = %r{<pre.+><code>Code block\.\.\.</code></pre>} + expect(helper.event_note(input)).to match(expected) + end - expect(event_note(input)).to match(expected) - end + it 'should truncate a single long line of text' do + text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars + input = text * 4 + expected = (text * 2).sub(/.{3}/, '...') - it 'should truncate a single long line of text' do - text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars - input = "#{text}#{text}#{text}#{text}" # 200 chars - expected = "#{text}#{text}".sub(/.{3}/, '...') + expect(helper.event_note(input)).to match(expected) + end - expect(event_note(input)).to match(expected) - end - - it 'should preserve a link href when link text is truncated' do - text = 'The quick brown fox jumped over the lazy dog' # 44 chars - input = "#{text}#{text}#{text} " # 133 chars - link_url = 'http://example.com/foo/bar/baz' # 30 chars - input << link_url - expected_link_text = 'http://example...</a>' + it 'should preserve a link href when link text is truncated' do + text = 'The quick brown fox jumped over the lazy dog' # 44 chars + input = "#{text}#{text}#{text} " # 133 chars + link_url = 'http://example.com/foo/bar/baz' # 30 chars + input << link_url + expected_link_text = 'http://example...</a>' - expect(event_note(input)).to match(link_url) - expect(event_note(input)).to match(expected_link_text) - end + expect(helper.event_note(input)).to match(link_url) + expect(helper.event_note(input)).to match(expected_link_text) + end - it 'should preserve code color scheme' do - input = "```ruby\ndef test\n 'hello world'\nend\n```" - expected = '<pre class="code highlight js-syntax-highlight ruby">' \ - "<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \ - " <span class=\"s1\">\'hello world\'</span>\n" \ - "<span class=\"k\">end</span>" \ - '</code></pre>' - expect(event_note(input)).to eq(expected) + it 'should preserve code color scheme' do + input = "```ruby\ndef test\n 'hello world'\nend\n```" + expected = '<pre class="code highlight js-syntax-highlight ruby">' \ + "<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \ + " <span class=\"s1\">\'hello world\'</span>\n" \ + "<span class=\"k\">end</span>" \ + '</code></pre>' + expect(helper.event_note(input)).to eq(expected) + end end end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 13de88e2f21..ade5c3b02d9 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -121,13 +121,14 @@ describe GitlabMarkdownHelper do before do @wiki = double('WikiPage') allow(@wiki).to receive(:content).and_return('wiki content') + allow(@wiki).to receive(:slug).and_return('nested/page') helper.instance_variable_set(:@project_wiki, @wiki) end it "should use Wiki pipeline for markdown files" do allow(@wiki).to receive(:format).and_return(:markdown) - expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki) + expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki, page_slug: "nested/page") helper.render_wiki_content(@wiki) end diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index bffe2c18b6f..eae61a54dfc 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -163,18 +163,15 @@ describe IssuesHelper do it { is_expected.to eq("!1, !2, or !3") } end - describe "note_active_class" do - before do - @note = create :note - @note1 = create :note - end + describe '#award_active_class' do + let!(:upvote) { create(:award_emoji) } it "returns empty string for unauthenticated user" do - expect(note_active_class(Note.all, nil)).to eq("") + expect(award_active_class(AwardEmoji.all, nil)).to eq("") end it "returns active string for author" do - expect(note_active_class(Note.all, @note.author)).to eq("active") + expect(award_active_class(AwardEmoji.all, upvote.user)).to eq("active") end end diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 600e1c4e9ec..a3336c87173 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -5,7 +5,7 @@ describe MergeRequestsHelper do let(:project) { create :project } let(:merge_request) { MergeRequest.new } let(:ci_service) { CiService.new } - let(:last_commit) { Ci::Commit.new({}) } + let(:last_commit) { Ci::Pipeline.new({}) } before do allow(merge_request).to receive(:source_project).and_return(project) @@ -17,7 +17,7 @@ describe MergeRequestsHelper do it 'does not include api credentials in a link' do allow(ci_service). to receive(:build_page).and_return("http://secretuser:secretpass@jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c") - expect(helper.ci_build_details_path(merge_request)).to_not match("secret") + expect(helper.ci_build_details_path(merge_request)).not_to match("secret") end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 29bcb8c5892..ac5af8740dc 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -88,18 +88,18 @@ describe ProjectsHelper do end describe 'default_clone_protocol' do - describe 'using HTTP' do + context 'when user is not logged in and gitlab protocol is HTTP' do it 'returns HTTP' do - expect(helper).to receive(:current_user).and_return(nil) + allow(helper).to receive(:current_user).and_return(nil) expect(helper.send(:default_clone_protocol)).to eq('http') end end - describe 'using HTTPS' do + context 'when user is not logged in and gitlab protocol is HTTPS' do it 'returns HTTPS' do - allow(Gitlab.config.gitlab).to receive(:protocol).and_return('https') - expect(helper).to receive(:current_user).and_return(nil) + stub_config_setting(protocol: 'https') + allow(helper).to receive(:current_user).and_return(nil) expect(helper.send(:default_clone_protocol)).to eq('https') end diff --git a/spec/javascripts/awards_handler_spec.js.coffee b/spec/javascripts/awards_handler_spec.js.coffee new file mode 100644 index 00000000000..ba191199dc7 --- /dev/null +++ b/spec/javascripts/awards_handler_spec.js.coffee @@ -0,0 +1,201 @@ +#= require awards_handler +#= require jquery +#= require jquery.cookie +#= require ./fixtures/emoji_menu + +awardsHandler = null +window.gl or= {} +window.gon or= {} +gl.emojiAliases = -> return { '+1': 'thumbsup', '-1': 'thumbsdown' } +gon.award_menu_url = '/emojis' + + +lazyAssert = (done, assertFn) -> + + setTimeout -> # Maybe jasmine.clock here? + assertFn() + done() + , 333 + + +describe 'AwardsHandler', -> + + fixture.preload 'awards_handler.html' + + beforeEach -> + fixture.load 'awards_handler.html' + awardsHandler = new AwardsHandler + spyOn(awardsHandler, 'postEmoji').and.callFake (url, emoji, cb) => cb() + spyOn(jQuery, 'get').and.callFake (req, cb) -> cb window.emojiMenu + + + describe '::showEmojiMenu', -> + + it 'should show emoji menu when Add emoji button clicked', (done) -> + + $('.js-add-award').eq(0).click() + + lazyAssert done, -> + $emojiMenu = $ '.emoji-menu' + expect($emojiMenu.length).toBe 1 + expect($emojiMenu.hasClass('is-visible')).toBe yes + expect($emojiMenu.find('#emoji_search').length).toBe 1 + expect($('.js-awards-block.current').length).toBe 1 + + + it 'should also show emoji menu for the smiley icon in notes', (done) -> + + $('.note-action-button').click() + + lazyAssert done, -> + $emojiMenu = $ '.emoji-menu' + expect($emojiMenu.length).toBe 1 + + + it 'should remove emoji menu when body is clicked', (done) -> + + $('.js-add-award').eq(0).click() + + lazyAssert done, -> + $emojiMenu = $('.emoji-menu') + $('body').click() + expect($emojiMenu.length).toBe 1 + expect($emojiMenu.hasClass('is-visible')).toBe no + expect($('.js-awards-block.current').length).toBe 0 + + + describe '::addAwardToEmojiBar', -> + + it 'should add emoji to votes block', -> + + $votesBlock = $('.js-awards-block').eq 0 + awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no + + $emojiButton = $votesBlock.find '[data-emoji=heart]' + + expect($emojiButton.length).toBe 1 + expect($emojiButton.next('.js-counter').text()).toBe '1' + expect($votesBlock.hasClass('hidden')).toBe no + + + it 'should remove the emoji when we click again', -> + + $votesBlock = $('.js-awards-block').eq 0 + awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no + awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no + $emojiButton = $votesBlock.find '[data-emoji=heart]' + + expect($emojiButton.length).toBe 0 + + + it 'should decrement the emoji counter', -> + + $votesBlock = $('.js-awards-block').eq 0 + awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no + + $emojiButton = $votesBlock.find '[data-emoji=heart]' + $emojiButton.next('.js-counter').text 5 + + awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no + + expect($emojiButton.length).toBe 1 + expect($emojiButton.next('.js-counter').text()).toBe '4' + + + describe '::getAwardUrl', -> + + it 'should return the url for request', -> + + expect(awardsHandler.getAwardUrl()).toBe '/gitlab-org/gitlab-test/issues/8/toggle_award_emoji' + + + describe '::addAward and ::checkMutuality', -> + + it 'should handle :+1: and :-1: mutuality', -> + + awardUrl = awardsHandler.getAwardUrl() + $votesBlock = $('.js-awards-block').eq 0 + $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent() + $thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent() + + awardsHandler.addAward $votesBlock, awardUrl, 'thumbsup', no + + expect($thumbsUpEmoji.hasClass('active')).toBe yes + expect($thumbsDownEmoji.hasClass('active')).toBe no + + $thumbsUpEmoji.tooltip() + $thumbsDownEmoji.tooltip() + + awardsHandler.addAward $votesBlock, awardUrl, 'thumbsdown', yes + + expect($thumbsUpEmoji.hasClass('active')).toBe no + expect($thumbsDownEmoji.hasClass('active')).toBe yes + + + describe '::removeEmoji', -> + + it 'should remove emoji', -> + + awardUrl = awardsHandler.getAwardUrl() + $votesBlock = $('.js-awards-block').eq 0 + + awardsHandler.addAward $votesBlock, awardUrl, 'fire', no + expect($votesBlock.find('[data-emoji=fire]').length).toBe 1 + + awardsHandler.removeEmoji $votesBlock.find('[data-emoji=fire]').closest('button') + expect($votesBlock.find('[data-emoji=fire]').length).toBe 0 + + + describe 'search', -> + + it 'should filter the emoji', -> + + $('.js-add-award').eq(0).click() + + expect($('[data-emoji=angel]').is(':visible')).toBe yes + expect($('[data-emoji=anger]').is(':visible')).toBe yes + + $('#emoji_search').val('ali').trigger 'keyup' + + expect($('[data-emoji=angel]').is(':visible')).toBe no + expect($('[data-emoji=anger]').is(':visible')).toBe no + expect($('[data-emoji=alien]').is(':visible')).toBe yes + expect($('h5.emoji-search').is(':visible')).toBe yes + + + describe 'emoji menu', -> + + selector = '[data-emoji=sunglasses]' + + openEmojiMenuAndAddEmoji = -> + + $('.js-add-award').eq(0).click() + + $menu = $ '.emoji-menu' + $block = $ '.js-awards-block' + $emoji = $menu.find ".emoji-menu-list-item #{selector}" + + expect($emoji.length).toBe 1 + expect($block.find(selector).length).toBe 0 + + $emoji.click() + + expect($menu.hasClass('.is-visible')).toBe no + expect($block.find(selector).length).toBe 1 + + + it 'should add selected emoji to awards block', -> + + openEmojiMenuAndAddEmoji() + + + it 'should remove already selected emoji', -> + + openEmojiMenuAndAddEmoji() + $('.js-add-award').eq(0).click() + + $block = $ '.js-awards-block' + $emoji = $('.emoji-menu').find ".emoji-menu-list-item #{selector}" + + $emoji.click() + expect($block.find(selector).length).toBe 0 diff --git a/spec/javascripts/behaviors/quick_submit_spec.js.coffee b/spec/javascripts/behaviors/quick_submit_spec.js.coffee index 09708c12ed4..d3b003a328a 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js.coffee +++ b/spec/javascripts/behaviors/quick_submit_spec.js.coffee @@ -14,17 +14,17 @@ describe 'Quick Submit behavior', -> } it 'does not respond to other keyCodes', -> - $('input').trigger(keydownEvent(keyCode: 32)) + $('input.quick-submit-input').trigger(keydownEvent(keyCode: 32)) expect(@spies.submit).not.toHaveBeenTriggered() it 'does not respond to Enter alone', -> - $('input').trigger(keydownEvent(ctrlKey: false, metaKey: false)) + $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: false, metaKey: false)) expect(@spies.submit).not.toHaveBeenTriggered() it 'does not respond to repeated events', -> - $('input').trigger(keydownEvent(repeat: true)) + $('input.quick-submit-input').trigger(keydownEvent(repeat: true)) expect(@spies.submit).not.toHaveBeenTriggered() @@ -38,26 +38,26 @@ describe 'Quick Submit behavior', -> # only run the tests that apply to the current platform if navigator.userAgent.match(/Macintosh/) it 'responds to Meta+Enter', -> - $('input').trigger(keydownEvent()) + $('input.quick-submit-input').trigger(keydownEvent()) expect(@spies.submit).toHaveBeenTriggered() it 'excludes other modifier keys', -> - $('input').trigger(keydownEvent(altKey: true)) - $('input').trigger(keydownEvent(ctrlKey: true)) - $('input').trigger(keydownEvent(shiftKey: true)) + $('input.quick-submit-input').trigger(keydownEvent(altKey: true)) + $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: true)) + $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true)) expect(@spies.submit).not.toHaveBeenTriggered() else it 'responds to Ctrl+Enter', -> - $('input').trigger(keydownEvent()) + $('input.quick-submit-input').trigger(keydownEvent()) expect(@spies.submit).toHaveBeenTriggered() it 'excludes other modifier keys', -> - $('input').trigger(keydownEvent(altKey: true)) - $('input').trigger(keydownEvent(metaKey: true)) - $('input').trigger(keydownEvent(shiftKey: true)) + $('input.quick-submit-input').trigger(keydownEvent(altKey: true)) + $('input.quick-submit-input').trigger(keydownEvent(metaKey: true)) + $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true)) expect(@spies.submit).not.toHaveBeenTriggered() diff --git a/spec/javascripts/fixtures/awards_handler.html.haml b/spec/javascripts/fixtures/awards_handler.html.haml new file mode 100644 index 00000000000..d55936ee4f9 --- /dev/null +++ b/spec/javascripts/fixtures/awards_handler.html.haml @@ -0,0 +1,52 @@ +.issue-details.issuable-details + .detail-page-description.content-block + %h2.title Quibusdam sint officiis earum molestiae ipsa autem voluptatem nisi rem. + .description.js-task-list-container.is-task-list-enabled + .wiki + %p Qui exercitationem magnam optio quae fuga earum odio. + %textarea.hidden.js-task-list-field Qui exercitationem magnam optio quae fuga earum odio. + %small.edited-text + .content-block.content-block-small + .awards.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/issues/8/toggle_award_emoji"} + %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"} + .icon.emoji-icon.emoji-1F44D{"data-aliases" => "", "data-emoji" => "thumbsup", "data-unicode-name" => "1F44D", :title => "thumbsup"} + %span.award-control-text.js-counter 0 + %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"} + .icon.emoji-icon.emoji-1F44E{"data-aliases" => "", "data-emoji" => "thumbsdown", "data-unicode-name" => "1F44E", :title => "thumbsdown"} + %span.award-control-text.js-counter 0 + .award-menu-holder.js-award-holder + %button.btn.award-control.js-add-award{:type => "button"} + %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal + %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading + %span.award-control-text Add + %section.issuable-discussion + #notes + %ul#notes-list.notes.main-notes-list.timeline + %li#note_348.note.note-row-348.timeline-entry{"data-author-id" => "18", "data-editable" => ""} + .timeline-entry-inner + .timeline-icon + %a{:href => "/u/agustin"} + %img.avatar.s40{:alt => "", :src => "#"}/ + .timeline-content + .note-header + %a.author_link{:href => "/u/agustin"} + %span.author Brenna Stokes + .inline.note-headline-light + @agustin commented + %a{:href => "#note_348"} + %time 11 days ago + .note-actions + %span.note-role Reporter + %a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"} + %i.fa.fa-spinner.fa-spin + %i.fa.fa-smile-o + .js-task-list-container.note-body.is-task-list-enabled + .note-text + %p Suscipit sunt quia quisquam sed eveniet ipsam. + .note-awards + .awards.hidden.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/notes/348/toggle_award_emoji"} + .award-menu-holder.js-award-holder + %button.btn.award-control.js-add-award{:type => "button"} + %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal + %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading + %span.award-control-text Add diff --git a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml index e3788bee813..dc2ceed42f4 100644 --- a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml +++ b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml @@ -1,5 +1,5 @@ %form.js-quick-submit{ action: '/foo' } - %input{ type: 'text' } + %input{ type: 'text', class: 'quick-submit-input'} %textarea %input{ type: 'submit'} Submit diff --git a/spec/javascripts/fixtures/emoji_menu.coffee b/spec/javascripts/fixtures/emoji_menu.coffee new file mode 100644 index 00000000000..e529dd5f1cd --- /dev/null +++ b/spec/javascripts/fixtures/emoji_menu.coffee @@ -0,0 +1,957 @@ +window.emojiMenu = """ + <div class='emoji-menu'> + <div class='emoji-menu-content'> + <input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" /> + <h5 class='emoji-menu-title'> + Emoticons + </h5> + <ul class='clearfix emoji-menu-list'> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F47D" title="alien" data-aliases="" data-emoji="alien" data-unicode-name="1F47D"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F47C" title="angel" data-aliases="" data-emoji="angel" data-unicode-name="1F47C"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F4A2" title="anger" data-aliases="" data-emoji="anger" data-unicode-name="1F4A2"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F620" title="angry" data-aliases="" data-emoji="angry" data-unicode-name="1F620"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F627" title="anguished" data-aliases="" data-emoji="anguished" data-unicode-name="1F627"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F632" title="astonished" data-aliases="" data-emoji="astonished" data-unicode-name="1F632"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F45F" title="athletic_shoe" data-aliases="" data-emoji="athletic_shoe" data-unicode-name="1F45F"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F476" title="baby" data-aliases="" data-emoji="baby" data-unicode-name="1F476"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F459" title="bikini" data-aliases="" data-emoji="bikini" data-unicode-name="1F459"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F499" title="blue_heart" data-aliases="" data-emoji="blue_heart" data-unicode-name="1F499"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F60A" title="blush" data-aliases="" data-emoji="blush" data-unicode-name="1F60A"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F4A5" title="boom" data-aliases="" data-emoji="boom" data-unicode-name="1F4A5"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F462" title="boot" data-aliases="" data-emoji="boot" data-unicode-name="1F462"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F647" title="bow" data-aliases="" data-emoji="bow" data-unicode-name="1F647"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F466" title="boy" data-aliases="" data-emoji="boy" data-unicode-name="1F466"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F470" title="bride_with_veil" data-aliases="" data-emoji="bride_with_veil" data-unicode-name="1F470"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F4BC" title="briefcase" data-aliases="" data-emoji="briefcase" data-unicode-name="1F4BC"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F494" title="broken_heart" data-aliases="" data-emoji="broken_heart" data-unicode-name="1F494"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F464" title="bust_in_silhouette" data-aliases="" data-emoji="bust_in_silhouette" data-unicode-name="1F464"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F465" title="busts_in_silhouette" data-aliases="" data-emoji="busts_in_silhouette" data-unicode-name="1F465"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F44F" title="clap" data-aliases="" data-emoji="clap" data-unicode-name="1F44F"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F302" title="closed_umbrella" data-aliases="" data-emoji="closed_umbrella" data-unicode-name="1F302"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F630" title="cold_sweat" data-aliases="" data-emoji="cold_sweat" data-unicode-name="1F630"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F616" title="confounded" data-aliases="" data-emoji="confounded" data-unicode-name="1F616"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F615" title="confused" data-aliases="" data-emoji="confused" data-unicode-name="1F615"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F477" title="construction_worker" data-aliases="" data-emoji="construction_worker" data-unicode-name="1F477"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F46E" title="cop" data-aliases="" data-emoji="cop" data-unicode-name="1F46E"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F46B" title="couple" data-aliases="" data-emoji="couple" data-unicode-name="1F46B"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F491" title="couple_with_heart" data-aliases="" data-emoji="couple_with_heart" data-unicode-name="1F491"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F48F" title="couplekiss" data-aliases="" data-emoji="couplekiss" data-unicode-name="1F48F"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F451" title="crown" data-aliases="" data-emoji="crown" data-unicode-name="1F451"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F622" title="cry" data-aliases="" data-emoji="cry" data-unicode-name="1F622"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F63F" title="crying_cat_face" data-aliases="" data-emoji="crying_cat_face" data-unicode-name="1F63F"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F498" title="cupid" data-aliases="" data-emoji="cupid" data-unicode-name="1F498"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F483" title="dancer" data-aliases="" data-emoji="dancer" data-unicode-name="1F483"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F46F" title="dancers" data-aliases="" data-emoji="dancers" data-unicode-name="1F46F"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F4A8" title="dash" data-aliases="" data-emoji="dash" data-unicode-name="1F4A8"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F61E" title="disappointed" data-aliases="" data-emoji="disappointed" data-unicode-name="1F61E"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F625" title="disappointed_relieved" data-aliases="" data-emoji="disappointed_relieved" data-unicode-name="1F625"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F4AB" title="dizzy" data-aliases="" data-emoji="dizzy" data-unicode-name="1F4AB"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F635" title="dizzy_face" data-aliases="" data-emoji="dizzy_face" data-unicode-name="1F635"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F457" title="dress" data-aliases="" data-emoji="dress" data-unicode-name="1F457"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F4A7" title="droplet" data-aliases="" data-emoji="droplet" data-unicode-name="1F4A7"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F442" title="ear" data-aliases="" data-emoji="ear" data-unicode-name="1F442"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F611" title="expressionless" data-aliases="" data-emoji="expressionless" data-unicode-name="1F611"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F453" title="eyeglasses" data-aliases="" data-emoji="eyeglasses" data-unicode-name="1F453"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F440" title="eyes" data-aliases="" data-emoji="eyes" data-unicode-name="1F440"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F46A" title="family" data-aliases="" data-emoji="family" data-unicode-name="1F46A"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F628" title="fearful" data-aliases="" data-emoji="fearful" data-unicode-name="1F628"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F525" title="fire" data-aliases=":flame:" data-emoji="fire" data-unicode-name="1F525"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-270A" title="fist" data-aliases="" data-emoji="fist" data-unicode-name="270A"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F633" title="flushed" data-aliases="" data-emoji="flushed" data-unicode-name="1F633"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F463" title="footprints" data-aliases="" data-emoji="footprints" data-unicode-name="1F463"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F626" title="frowning" data-aliases=":anguished:" data-emoji="frowning" data-unicode-name="1F626"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F48E" title="gem" data-aliases="" data-emoji="gem" data-unicode-name="1F48E"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F467" title="girl" data-aliases="" data-emoji="girl" data-unicode-name="1F467"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F49A" title="green_heart" data-aliases="" data-emoji="green_heart" data-unicode-name="1F49A"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F62C" title="grimacing" data-aliases="" data-emoji="grimacing" data-unicode-name="1F62C"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F601" title="grin" data-aliases="" data-emoji="grin" data-unicode-name="1F601"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F600" title="grinning" data-aliases="" data-emoji="grinning" data-unicode-name="1F600"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F482" title="guardsman" data-aliases="" data-emoji="guardsman" data-unicode-name="1F482"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F487" title="haircut" data-aliases="" data-emoji="haircut" data-unicode-name="1F487"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F45C" title="handbag" data-aliases="" data-emoji="handbag" data-unicode-name="1F45C"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F649" title="hear_no_evil" data-aliases="" data-emoji="hear_no_evil" data-unicode-name="1F649"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-2764" title="heart" data-aliases="" data-emoji="heart" data-unicode-name="2764"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F60D" title="heart_eyes" data-aliases="" data-emoji="heart_eyes" data-unicode-name="1F60D"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F63B" title="heart_eyes_cat" data-aliases="" data-emoji="heart_eyes_cat" data-unicode-name="1F63B"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F493" title="heartbeat" data-aliases="" data-emoji="heartbeat" data-unicode-name="1F493"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F497" title="heartpulse" data-aliases="" data-emoji="heartpulse" data-unicode-name="1F497"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F460" title="high_heel" data-aliases="" data-emoji="high_heel" data-unicode-name="1F460"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F62F" title="hushed" data-aliases="" data-emoji="hushed" data-unicode-name="1F62F"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F47F" title="imp" data-aliases="" data-emoji="imp" data-unicode-name="1F47F"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F481" title="information_desk_person" data-aliases="" data-emoji="information_desk_person" data-unicode-name="1F481"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F607" title="innocent" data-aliases="" data-emoji="innocent" data-unicode-name="1F607"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F47A" title="japanese_goblin" data-aliases="" data-emoji="japanese_goblin" data-unicode-name="1F47A"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F479" title="japanese_ogre" data-aliases="" data-emoji="japanese_ogre" data-unicode-name="1F479"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F456" title="jeans" data-aliases="" data-emoji="jeans" data-unicode-name="1F456"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F602" title="joy" data-aliases="" data-emoji="joy" data-unicode-name="1F602"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F639" title="joy_cat" data-aliases="" data-emoji="joy_cat" data-unicode-name="1F639"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F458" title="kimono" data-aliases="" data-emoji="kimono" data-unicode-name="1F458"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F48B" title="kiss" data-aliases="" data-emoji="kiss" data-unicode-name="1F48B"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F617" title="kissing" data-aliases="" data-emoji="kissing" data-unicode-name="1F617"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F63D" title="kissing_cat" data-aliases="" data-emoji="kissing_cat" data-unicode-name="1F63D"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F61A" title="kissing_closed_eyes" data-aliases="" data-emoji="kissing_closed_eyes" data-unicode-name="1F61A"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F618" title="kissing_heart" data-aliases="" data-emoji="kissing_heart" data-unicode-name="1F618"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F619" title="kissing_smiling_eyes" data-aliases="" data-emoji="kissing_smiling_eyes" data-unicode-name="1F619"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F606" title="laughing" data-aliases=":satisfied:" data-emoji="laughing" data-unicode-name="1F606"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F444" title="lips" data-aliases="" data-emoji="lips" data-unicode-name="1F444"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F484" title="lipstick" data-aliases="" data-emoji="lipstick" data-unicode-name="1F484"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F48C" title="love_letter" data-aliases="" data-emoji="love_letter" data-unicode-name="1F48C"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F468" title="man" data-aliases="" data-emoji="man" data-unicode-name="1F468"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F472" title="man_with_gua_pi_mao" data-aliases="" data-emoji="man_with_gua_pi_mao" data-unicode-name="1F472"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F473" title="man_with_turban" data-aliases="" data-emoji="man_with_turban" data-unicode-name="1F473"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F45E" title="mans_shoe" data-aliases="" data-emoji="mans_shoe" data-unicode-name="1F45E"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F637" title="mask" data-aliases="" data-emoji="mask" data-unicode-name="1F637"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F486" title="massage" data-aliases="" data-emoji="massage" data-unicode-name="1F486"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F4AA" title="muscle" data-aliases="" data-emoji="muscle" data-unicode-name="1F4AA"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F485" title="nail_care" data-aliases="" data-emoji="nail_care" data-unicode-name="1F485"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F454" title="necktie" data-aliases="" data-emoji="necktie" data-unicode-name="1F454"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F610" title="neutral_face" data-aliases="" data-emoji="neutral_face" data-unicode-name="1F610"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F645" title="no_good" data-aliases="" data-emoji="no_good" data-unicode-name="1F645"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F636" title="no_mouth" data-aliases="" data-emoji="no_mouth" data-unicode-name="1F636"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F443" title="nose" data-aliases="" data-emoji="nose" data-unicode-name="1F443"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F44C" title="ok_hand" data-aliases="" data-emoji="ok_hand" data-unicode-name="1F44C"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F646" title="ok_woman" data-aliases="" data-emoji="ok_woman" data-unicode-name="1F646"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F474" title="older_man" data-aliases="" data-emoji="older_man" data-unicode-name="1F474"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F475" title="older_woman" data-aliases=":grandma:" data-emoji="older_woman" data-unicode-name="1F475"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F450" title="open_hands" data-aliases="" data-emoji="open_hands" data-unicode-name="1F450"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F62E" title="open_mouth" data-aliases="" data-emoji="open_mouth" data-unicode-name="1F62E"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F614" title="pensive" data-aliases="" data-emoji="pensive" data-unicode-name="1F614"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F623" title="persevere" data-aliases="" data-emoji="persevere" data-unicode-name="1F623"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F64D" title="person_frowning" data-aliases="" data-emoji="person_frowning" data-unicode-name="1F64D"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F471" title="person_with_blond_hair" data-aliases="" data-emoji="person_with_blond_hair" data-unicode-name="1F471"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F64E" title="person_with_pouting_face" data-aliases="" data-emoji="person_with_pouting_face" data-unicode-name="1F64E"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F447" title="point_down" data-aliases="" data-emoji="point_down" data-unicode-name="1F447"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F448" title="point_left" data-aliases="" data-emoji="point_left" data-unicode-name="1F448"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F449" title="point_right" data-aliases="" data-emoji="point_right" data-unicode-name="1F449"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-261D" title="point_up" data-aliases="" data-emoji="point_up" data-unicode-name="261D"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F446" title="point_up_2" data-aliases="" data-emoji="point_up_2" data-unicode-name="1F446"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F4A9" title="poop" data-aliases=":shit: :hankey: :poo:" data-emoji="poop" data-unicode-name="1F4A9"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F45D" title="pouch" data-aliases="" data-emoji="pouch" data-unicode-name="1F45D"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F63E" title="pouting_cat" data-aliases="" data-emoji="pouting_cat" data-unicode-name="1F63E"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F64F" title="pray" data-aliases="" data-emoji="pray" data-unicode-name="1F64F"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F478" title="princess" data-aliases="" data-emoji="princess" data-unicode-name="1F478"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F44A" title="punch" data-aliases="" data-emoji="punch" data-unicode-name="1F44A"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F49C" title="purple_heart" data-aliases="" data-emoji="purple_heart" data-unicode-name="1F49C"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F45B" title="purse" data-aliases="" data-emoji="purse" data-unicode-name="1F45B"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F621" title="rage" data-aliases="" data-emoji="rage" data-unicode-name="1F621"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-270B" title="raised_hand" data-aliases="" data-emoji="raised_hand" data-unicode-name="270B"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F64C" title="raised_hands" data-aliases="" data-emoji="raised_hands" data-unicode-name="1F64C"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F64B" title="raising_hand" data-aliases="" data-emoji="raising_hand" data-unicode-name="1F64B"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-263A" title="relaxed" data-aliases="" data-emoji="relaxed" data-unicode-name="263A"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F60C" title="relieved" data-aliases="" data-emoji="relieved" data-unicode-name="1F60C"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F49E" title="revolving_hearts" data-aliases="" data-emoji="revolving_hearts" data-unicode-name="1F49E"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F380" title="ribbon" data-aliases="" data-emoji="ribbon" data-unicode-name="1F380"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F48D" title="ring" data-aliases="" data-emoji="ring" data-unicode-name="1F48D"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F3C3" title="runner" data-aliases="" data-emoji="runner" data-unicode-name="1F3C3"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F3BD" title="running_shirt_with_sash" data-aliases="" data-emoji="running_shirt_with_sash" data-unicode-name="1F3BD"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F461" title="sandal" data-aliases="" data-emoji="sandal" data-unicode-name="1F461"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F631" title="scream" data-aliases="" data-emoji="scream" data-unicode-name="1F631"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F640" title="scream_cat" data-aliases="" data-emoji="scream_cat" data-unicode-name="1F640"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F648" title="see_no_evil" data-aliases="" data-emoji="see_no_evil" data-unicode-name="1F648"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F455" title="shirt" data-aliases="" data-emoji="shirt" data-unicode-name="1F455"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F480" title="skull" data-aliases=":skeleton:" data-emoji="skull" data-unicode-name="1F480"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F634" title="sleeping" data-aliases="" data-emoji="sleeping" data-unicode-name="1F634"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F62A" title="sleepy" data-aliases="" data-emoji="sleepy" data-unicode-name="1F62A"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F604" title="smile" data-aliases="" data-emoji="smile" data-unicode-name="1F604"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F638" title="smile_cat" data-aliases="" data-emoji="smile_cat" data-unicode-name="1F638"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F603" title="smiley" data-aliases="" data-emoji="smiley" data-unicode-name="1F603"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F63A" title="smiley_cat" data-aliases="" data-emoji="smiley_cat" data-unicode-name="1F63A"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F608" title="smiling_imp" data-aliases="" data-emoji="smiling_imp" data-unicode-name="1F608"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F60F" title="smirk" data-aliases="" data-emoji="smirk" data-unicode-name="1F60F"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F63C" title="smirk_cat" data-aliases="" data-emoji="smirk_cat" data-unicode-name="1F63C"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F62D" title="sob" data-aliases="" data-emoji="sob" data-unicode-name="1F62D"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-2728" title="sparkles" data-aliases="" data-emoji="sparkles" data-unicode-name="2728"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F496" title="sparkling_heart" data-aliases="" data-emoji="sparkling_heart" data-unicode-name="1F496"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F64A" title="speak_no_evil" data-aliases="" data-emoji="speak_no_evil" data-unicode-name="1F64A"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F4AC" title="speech_balloon" data-aliases="" data-emoji="speech_balloon" data-unicode-name="1F4AC"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F31F" title="star2" data-aliases="" data-emoji="star2" data-unicode-name="1F31F"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F61B" title="stuck_out_tongue" data-aliases="" data-emoji="stuck_out_tongue" data-unicode-name="1F61B"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F61D" title="stuck_out_tongue_closed_eyes" data-aliases="" data-emoji="stuck_out_tongue_closed_eyes" data-unicode-name="1F61D"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F61C" title="stuck_out_tongue_winking_eye" data-aliases="" data-emoji="stuck_out_tongue_winking_eye" data-unicode-name="1F61C"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F60E" title="sunglasses" data-aliases="" data-emoji="sunglasses" data-unicode-name="1F60E"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F613" title="sweat" data-aliases="" data-emoji="sweat" data-unicode-name="1F613"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F4A6" title="sweat_drops" data-aliases="" data-emoji="sweat_drops" data-unicode-name="1F4A6"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F605" title="sweat_smile" data-aliases="" data-emoji="sweat_smile" data-unicode-name="1F605"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F4AD" title="thought_balloon" data-aliases="" data-emoji="thought_balloon" data-unicode-name="1F4AD"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F44E" title="thumbsdown" data-aliases=":-1:" data-emoji="thumbsdown" data-unicode-name="1F44E"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F44D" title="thumbsup" data-aliases=":+1:" data-emoji="thumbsup" data-unicode-name="1F44D"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F62B" title="tired_face" data-aliases="" data-emoji="tired_face" data-unicode-name="1F62B"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F445" title="tongue" data-aliases="" data-emoji="tongue" data-unicode-name="1F445"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F3A9" title="tophat" data-aliases="" data-emoji="tophat" data-unicode-name="1F3A9"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F624" title="triumph" data-aliases="" data-emoji="triumph" data-unicode-name="1F624"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F495" title="two_hearts" data-aliases="" data-emoji="two_hearts" data-unicode-name="1F495"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F46C" title="two_men_holding_hands" data-aliases="" data-emoji="two_men_holding_hands" data-unicode-name="1F46C"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F46D" title="two_women_holding_hands" data-aliases="" data-emoji="two_women_holding_hands" data-unicode-name="1F46D"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F612" title="unamused" data-aliases="" data-emoji="unamused" data-unicode-name="1F612"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-270C" title="v" data-aliases="" data-emoji="v" data-unicode-name="270C"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F6B6" title="walking" data-aliases="" data-emoji="walking" data-unicode-name="1F6B6"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F44B" title="wave" data-aliases="" data-emoji="wave" data-unicode-name="1F44B"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F629" title="weary" data-aliases="" data-emoji="weary" data-unicode-name="1F629"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F609" title="wink" data-aliases="" data-emoji="wink" data-unicode-name="1F609"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F469" title="woman" data-aliases="" data-emoji="woman" data-unicode-name="1F469"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F45A" title="womans_clothes" data-aliases="" data-emoji="womans_clothes" data-unicode-name="1F45A"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F452" title="womans_hat" data-aliases="" data-emoji="womans_hat" data-unicode-name="1F452"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F61F" title="worried" data-aliases="" data-emoji="worried" data-unicode-name="1F61F"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F49B" title="yellow_heart" data-aliases="" data-emoji="yellow_heart" data-unicode-name="1F49B"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F60B" title="yum" data-aliases="" data-emoji="yum" data-unicode-name="1F60B"></div> + </button> + </li> + <li class='pull-left text-center emoji-menu-list-item'> + <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> + <div class="icon emoji-icon emoji-1F4A4" title="zzz" data-aliases="" data-emoji="zzz" data-unicode-name="1F4A4"></div> + </button> + </li> + </ul> + </div> + </div> +""" diff --git a/spec/javascripts/fixtures/right_sidebar.html.haml b/spec/javascripts/fixtures/right_sidebar.html.haml new file mode 100644 index 00000000000..95efaff4b69 --- /dev/null +++ b/spec/javascripts/fixtures/right_sidebar.html.haml @@ -0,0 +1,13 @@ +%div + %div.page-gutter.page-with-sidebar + + %aside.right-sidebar + %div.block.issuable-sidebar-header + %a.gutter-toggle.pull-right.js-sidebar-toggle + %i.fa.fa-angle-double-left + + %form.issuable-context-form + %div.block.labels + %div.sidebar-collapsed-icon + %i.fa.fa-tags + %span 1 diff --git a/spec/javascripts/fixtures/u2f/authenticate.html.haml b/spec/javascripts/fixtures/u2f/authenticate.html.haml new file mode 100644 index 00000000000..859e79a6c9e --- /dev/null +++ b/spec/javascripts/fixtures/u2f/authenticate.html.haml @@ -0,0 +1 @@ += render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in" } diff --git a/spec/javascripts/fixtures/u2f/register.html.haml b/spec/javascripts/fixtures/u2f/register.html.haml new file mode 100644 index 00000000000..393c0613fd3 --- /dev/null +++ b/spec/javascripts/fixtures/u2f/register.html.haml @@ -0,0 +1 @@ += render partial: "u2f/register", locals: { create_u2f_profile_two_factor_auth_path: '/profile/two_factor_auth/create_u2f' } diff --git a/spec/javascripts/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index 78d39f1b428..82ee1954a59 100644 --- a/spec/javascripts/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -1,4 +1,4 @@ -//= require stat_graph_contributors_graph +//= require graphs/stat_graph_contributors_graph describe("ContributorsGraph", function () { describe("#set_x_domain", function () { diff --git a/spec/javascripts/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index dbafe782b77..56970e22e34 100644 --- a/spec/javascripts/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,4 +1,4 @@ -//= require stat_graph_contributors_util +//= require graphs/stat_graph_contributors_util describe("ContributorsStatGraphUtil", function () { @@ -9,14 +9,14 @@ describe("ContributorsStatGraphUtil", function () { {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1}, {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3}, {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3}] - + var correct_parsed_log = { total: [ {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], by_author: [ - { + { author_name: "Karlo Soriano", author_email: "karlo@email.com", "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} }, @@ -132,8 +132,8 @@ describe("ContributorsStatGraphUtil", function () { total: [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], by_author:[ - { - author: "Karlo Soriano", + { + author: "Karlo Soriano", "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} }, { @@ -161,11 +161,11 @@ describe("ContributorsStatGraphUtil", function () { it("returns the log by author sorted by specified field", function () { var fake_parsed_log = { total: [ - {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, + {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} ], by_author: [ - { + { author_name: "Karlo Soriano", author_email: "karlo@email.com", "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} }, diff --git a/spec/javascripts/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js index 4c652910cd6..4b05d401a42 100644 --- a/spec/javascripts/stat_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_spec.js @@ -1,4 +1,4 @@ -//= require stat_graph +//= require graphs/stat_graph describe("StatGraph", function () { diff --git a/spec/javascripts/new_branch_spec.js.coffee b/spec/javascripts/new_branch_spec.js.coffee index f2ce85efcdc..ce773793817 100644 --- a/spec/javascripts/new_branch_spec.js.coffee +++ b/spec/javascripts/new_branch_spec.js.coffee @@ -1,4 +1,4 @@ -#= require jquery-ui +#= require jquery-ui/autocomplete #= require new_branch_form describe 'Branch', -> diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee index 3d8de2ff989..1cf34d4d2d3 100644 --- a/spec/javascripts/project_title_spec.js.coffee +++ b/spec/javascripts/project_title_spec.js.coffee @@ -1,5 +1,6 @@ #= require bootstrap #= require select2 +#= require lib/type_utility #= require gl_dropdown #= require api #= require project_select diff --git a/spec/javascripts/right_sidebar_spec.js.coffee b/spec/javascripts/right_sidebar_spec.js.coffee new file mode 100644 index 00000000000..2075cacdb67 --- /dev/null +++ b/spec/javascripts/right_sidebar_spec.js.coffee @@ -0,0 +1,69 @@ +#= require right_sidebar +#= require jquery +#= require jquery.cookie + +@sidebar = null +$aside = null +$toggle = null +$icon = null +$page = null +$labelsIcon = null + + +assertSidebarState = (state) -> + + shouldBeExpanded = state is 'expanded' + shouldBeCollapsed = state is 'collapsed' + + expect($aside.hasClass('right-sidebar-expanded')).toBe shouldBeExpanded + expect($page.hasClass('right-sidebar-expanded')).toBe shouldBeExpanded + expect($icon.hasClass('fa-angle-double-right')).toBe shouldBeExpanded + + expect($aside.hasClass('right-sidebar-collapsed')).toBe shouldBeCollapsed + expect($page.hasClass('right-sidebar-collapsed')).toBe shouldBeCollapsed + expect($icon.hasClass('fa-angle-double-left')).toBe shouldBeCollapsed + + +describe 'RightSidebar', -> + + fixture.preload 'right_sidebar.html' + + beforeEach -> + fixture.load 'right_sidebar.html' + + @sidebar = new Sidebar + $aside = $ '.right-sidebar' + $page = $ '.page-with-sidebar' + $icon = $aside.find 'i' + $toggle = $aside.find '.js-sidebar-toggle' + $labelsIcon = $aside.find '.sidebar-collapsed-icon' + + + it 'should expand the sidebar when arrow is clicked', -> + + $toggle.click() + assertSidebarState 'expanded' + + + it 'should collapse the sidebar when arrow is clicked', -> + + $toggle.click() + assertSidebarState 'expanded' + + $toggle.click() + assertSidebarState 'collapsed' + + + it 'should float over the page and when sidebar icons clicked', -> + + $labelsIcon.click() + assertSidebarState 'expanded' + + + it 'should collapse when the icon arrow clicked while it is floating on page', -> + + $labelsIcon.click() + assertSidebarState 'expanded' + + $toggle.click() + assertSidebarState 'collapsed' diff --git a/spec/javascripts/u2f/authenticate_spec.coffee b/spec/javascripts/u2f/authenticate_spec.coffee new file mode 100644 index 00000000000..e8a2892d678 --- /dev/null +++ b/spec/javascripts/u2f/authenticate_spec.coffee @@ -0,0 +1,52 @@ +#= require u2f/authenticate +#= require u2f/util +#= require u2f/error +#= require u2f +#= require ./mock_u2f_device + +describe 'U2FAuthenticate', -> + U2FUtil.enableTestMode() + fixture.load('u2f/authenticate') + + beforeEach -> + @u2fDevice = new MockU2FDevice + @container = $("#js-authenticate-u2f") + @component = new U2FAuthenticate(@container, {}, "token") + @component.start() + + it 'allows authenticating via a U2F device', -> + setupButton = @container.find("#js-login-u2f-device") + setupMessage = @container.find("p") + expect(setupMessage.text()).toContain('Insert your security key') + expect(setupButton.text()).toBe('Login Via U2F Device') + setupButton.trigger('click') + + inProgressMessage = @container.find("p") + expect(inProgressMessage.text()).toContain("Trying to communicate with your device") + + @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"}) + authenticatedMessage = @container.find("p") + deviceResponse = @container.find('#js-device-response') + expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server") + expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}') + + describe "errors", -> + it "displays an error message", -> + setupButton = @container.find("#js-login-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"}) + errorMessage = @container.find("p") + expect(errorMessage.text()).toContain("There was a problem communicating with your device") + + it "allows retrying authentication after an error", -> + setupButton = @container.find("#js-login-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"}) + retryButton = @container.find("#js-u2f-try-again") + retryButton.trigger('click') + + setupButton = @container.find("#js-login-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"}) + authenticatedMessage = @container.find("p") + expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server") diff --git a/spec/javascripts/u2f/mock_u2f_device.js.coffee b/spec/javascripts/u2f/mock_u2f_device.js.coffee new file mode 100644 index 00000000000..97ed0e83a0e --- /dev/null +++ b/spec/javascripts/u2f/mock_u2f_device.js.coffee @@ -0,0 +1,15 @@ +class @MockU2FDevice + constructor: () -> + window.u2f ||= {} + + window.u2f.register = (appId, registerRequests, signRequests, callback) => + @registerCallback = callback + + window.u2f.sign = (appId, challenges, signRequests, callback) => + @authenticateCallback = callback + + respondToRegisterRequest: (params) => + @registerCallback(params) + + respondToAuthenticateRequest: (params) => + @authenticateCallback(params) diff --git a/spec/javascripts/u2f/register_spec.js.coffee b/spec/javascripts/u2f/register_spec.js.coffee new file mode 100644 index 00000000000..0858abeca1a --- /dev/null +++ b/spec/javascripts/u2f/register_spec.js.coffee @@ -0,0 +1,57 @@ +#= require u2f/register +#= require u2f/util +#= require u2f/error +#= require u2f +#= require ./mock_u2f_device + +describe 'U2FRegister', -> + U2FUtil.enableTestMode() + fixture.load('u2f/register') + + beforeEach -> + @u2fDevice = new MockU2FDevice + @container = $("#js-register-u2f") + @component = new U2FRegister(@container, $("#js-register-u2f-templates"), {}, "token") + @component.start() + + it 'allows registering a U2F device', -> + setupButton = @container.find("#js-setup-u2f-device") + expect(setupButton.text()).toBe('Setup New U2F Device') + setupButton.trigger('click') + + inProgressMessage = @container.children("p") + expect(inProgressMessage.text()).toContain("Trying to communicate with your device") + + @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"}) + registeredMessage = @container.find('p') + deviceResponse = @container.find('#js-device-response') + expect(registeredMessage.text()).toContain("Your device was successfully set up!") + expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}') + + describe "errors", -> + it "doesn't allow the same device to be registered twice (for the same user", -> + setupButton = @container.find("#js-setup-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToRegisterRequest({errorCode: 4}) + errorMessage = @container.find("p") + expect(errorMessage.text()).toContain("already been registered with us") + + it "displays an error message for other errors", -> + setupButton = @container.find("#js-setup-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToRegisterRequest({errorCode: "error!"}) + errorMessage = @container.find("p") + expect(errorMessage.text()).toContain("There was a problem communicating with your device") + + it "allows retrying registration after an error", -> + setupButton = @container.find("#js-setup-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToRegisterRequest({errorCode: "error!"}) + retryButton = @container.find("#U2FTryAgain") + retryButton.trigger('click') + + setupButton = @container.find("#js-setup-u2f-device") + setupButton.trigger('click') + @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"}) + registeredMessage = @container.find("p") + expect(registeredMessage.text()).toContain("Your device was successfully set up!") diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb index c2a8ad36c30..593bd6d5cac 100644 --- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb @@ -98,11 +98,6 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do expect(link).not_to match %r(https?://) expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true) end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit_range]).not_to be_empty - end end context 'cross-project reference' do @@ -135,11 +130,6 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" expect(reference_filter(act).to_html).to eq exp end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit_range]).not_to be_empty - end end context 'cross-project URL reference' do @@ -173,10 +163,5 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" expect(reference_filter(act).to_html).to eq exp end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit_range]).not_to be_empty - end end end diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb index 63a32d9d455..d46d3f1489e 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb @@ -93,11 +93,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do expect(link).not_to match %r(https?://) expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true) end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit]).not_to be_empty - end end context 'cross-project reference' do @@ -124,11 +119,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do exp = act = "Committed #{invalidate_reference(reference)}" expect(reference_filter(act).to_html).to eq exp end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit]).not_to be_empty - end end context 'cross-project URL reference' do @@ -154,10 +144,5 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do act = "Committed #{invalidate_reference(reference)}" expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/) end - - it 'adds to the results hash' do - result = reference_pipeline_result("See #{reference}") - expect(result[:references][:commit]).not_to be_empty - end end end diff --git a/spec/lib/banzai/filter/inline_diff_filter_spec.rb b/spec/lib/banzai/filter/inline_diff_filter_spec.rb new file mode 100644 index 00000000000..9e526371294 --- /dev/null +++ b/spec/lib/banzai/filter/inline_diff_filter_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe Banzai::Filter::InlineDiffFilter, lib: true do + include FilterSpecHelper + + it 'adds inline diff span tags for deletions when using square brackets' do + doc = "START [-something deleted-] END" + expect(filter(doc).to_html).to eq('START <span class="idiff left right deletion">something deleted</span> END') + end + + it 'adds inline diff span tags for deletions when using curley braces' do + doc = "START {-something deleted-} END" + expect(filter(doc).to_html).to eq('START <span class="idiff left right deletion">something deleted</span> END') + end + + it 'does not add inline diff span tags when a closing tag is not provided' do + doc = "START [- END" + expect(filter(doc).to_html).to eq(doc) + end + + it 'adds inline span tags for additions when using square brackets' do + doc = "START [+something added+] END" + expect(filter(doc).to_html).to eq('START <span class="idiff left right addition">something added</span> END') + end + + it 'adds inline span tags for additions when using curley braces' do + doc = "START {+something added+} END" + expect(filter(doc).to_html).to eq('START <span class="idiff left right addition">something added</span> END') + end + + it 'does not add inline diff span tags when a closing addition tag is not provided' do + doc = "START {+ END" + expect(filter(doc).to_html).to eq(doc) + end + + it 'does not add inline diff span tags when the tags do not match' do + examples = [ + "{+ additions +]", + "[+ additions +}", + "{- delletions -]", + "[- delletions -}" + ] + + examples.each do |doc| + expect(filter(doc).to_html).to eq(doc) + end + end + + it 'prevents user-land html being injected' do + doc = "START {+<script>alert('I steal cookies')</script>+} END" + expect(filter(doc).to_html).to eq("START <span class=\"idiff left right addition\"><script>alert('I steal cookies')</script></span> END") + end + + it 'preserves content inside pre tags' do + doc = "<pre>START {+something added+} END</pre>" + expect(filter(doc).to_html).to eq(doc) + end + + it 'preserves content inside code tags' do + doc = "<code>START {+something added+} END</code>" + expect(filter(doc).to_html).to eq(doc) + end + + it 'preserves content inside tt tags' do + doc = "<tt>START {+something added+} END</tt>" + expect(filter(doc).to_html).to eq(doc) + end +end diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 266ebef33d6..8e6a264970d 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -91,11 +91,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true) end - it 'adds to the results hash' do - result = reference_pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end - it 'does not process links containing issue numbers followed by text' do href = "#{reference}st" doc = reference_filter("<a href='#{href}'></a>") @@ -136,11 +131,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do expect(reference_filter(act).to_html).to eq exp end - - it 'adds to the results hash' do - result = reference_pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end end context 'cross-project URL reference' do @@ -160,11 +150,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do doc = reference_filter("Fixed (#{reference}.)") expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/) end - - it 'adds to the results hash' do - result = reference_pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end end context 'cross-project reference in link href' do @@ -184,11 +169,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do doc = reference_filter("Fixed (#{reference}.)") expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) end - - it 'adds to the results hash' do - result = reference_pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end end context 'cross-project URL in link href' do @@ -208,10 +188,5 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do doc = reference_filter("Fixed (#{reference}.)") expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) end - - it 'adds to the results hash' do - result = reference_pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end end end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index b0a38e7c251..f1064a701d8 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -48,11 +48,6 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name) end - it 'adds to the results hash' do - result = reference_pipeline_result("Label #{reference}") - expect(result[:references][:label]).to eq [label] - end - describe 'label span element' do it 'includes default classes' do doc = reference_filter("Label #{reference}") @@ -170,11 +165,6 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do expect(link).to have_attribute('data-label') expect(link.attr('data-label')).to eq label.id.to_s end - - it 'adds to the results hash' do - result = reference_pipeline_result("Label #{reference}") - expect(result[:references][:label]).to eq [label] - end end describe 'cross project label references' do diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index 352710df307..3185e41fe5c 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -78,11 +78,6 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do expect(link).not_to match %r(https?://) expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true) end - - it 'adds to the results hash' do - result = reference_pipeline_result("Merge #{reference}") - expect(result[:references][:merge_request]).to eq [merge] - end end context 'cross-project reference' do @@ -109,11 +104,6 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do expect(reference_filter(act).to_html).to eq exp end - - it 'adds to the results hash' do - result = reference_pipeline_result("Merge #{reference}") - expect(result[:references][:merge_request]).to eq [merge] - end end context 'cross-project URL reference' do @@ -133,10 +123,5 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do doc = reference_filter("Merge (#{reference}.)") expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/) end - - it 'adds to the results hash' do - result = reference_pipeline_result("Merge #{reference}") - expect(result[:references][:merge_request]).to eq [merge] - end end end diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index ebf3d7489b5..9424f2363e1 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -3,8 +3,9 @@ require 'spec_helper' describe Banzai::Filter::MilestoneReferenceFilter, lib: true do include FilterSpecHelper - let(:project) { create(:project, :public) } - let(:milestone) { create(:milestone, project: project) } + let(:project) { create(:project, :public) } + let(:milestone) { create(:milestone, project: project) } + let(:reference) { milestone.to_reference } it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) @@ -17,11 +18,37 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do end end - context 'internal reference' do - # Convert the Markdown link to only the URL, since these tests aren't run through the regular Markdown pipeline. - # Milestone reference behavior in the full Markdown pipeline is tested elsewhere. - let(:reference) { milestone.to_reference.gsub(/\[([^\]]+)\]\(([^)]+)\)/, '\2') } + it 'includes default classes' do + doc = reference_filter("Milestone #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone' + end + + it 'includes a data-project attribute' do + doc = reference_filter("Milestone #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-milestone attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-milestone') + expect(link.attr('data-milestone')).to eq milestone.id.to_s + end + + it 'supports an :only_path context' do + doc = reference_filter("Milestone #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + expect(link).not_to match %r(https?://) + expect(link).to eq urls. + namespace_project_milestone_path(project.namespace, project, milestone) + end + + context 'Integer-based references' do it 'links to a valid reference' do doc = reference_filter("See #{reference}") @@ -30,29 +57,82 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do end it 'links with adjacent text' do - doc = reference_filter("milestone (#{reference}.)") - expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(milestone.title)}<\/a>\.\)/) + doc = reference_filter("Milestone (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+>#{milestone.name}</a>\.\))) end - it 'includes a title attribute' do - doc = reference_filter("milestone #{reference}") - expect(doc.css('a').first.attr('title')).to eq "Milestone: #{milestone.title}" + it 'ignores invalid milestone IIDs' do + exp = act = "Milestone #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq exp end + end + + context 'String-based single-word references' do + let(:milestone) { create(:milestone, name: 'gfm', project: project) } + let(:reference) { "#{Milestone.reference_prefix}#{milestone.name}" } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") - it 'escapes the title attribute' do - milestone.update_attribute(:title, %{"></a>whatever<a title="}) + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(project.namespace, project, milestone) + expect(doc.text).to eq 'See gfm' + end - doc = reference_filter("milestone #{reference}") - expect(doc.text).to eq "milestone #{milestone.title}" + it 'links with adjacent text' do + doc = reference_filter("Milestone (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+>#{milestone.name}</a>\.\))) end - it 'includes default classes' do - doc = reference_filter("milestone #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone' + it 'ignores invalid milestone names' do + exp = act = "Milestone #{Milestone.reference_prefix}#{milestone.name.reverse}" + + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'String-based multi-word references in quotes' do + let(:milestone) { create(:milestone, name: 'gfm references', project: project) } + let(:reference) { milestone.to_reference(format: :name) } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(project.namespace, project, milestone) + expect(doc.text).to eq 'See gfm references' + end + + it 'links with adjacent text' do + doc = reference_filter("Milestone (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+>#{milestone.name}</a>\.\))) + end + + it 'ignores invalid milestone names' do + exp = act = %(Milestone #{Milestone.reference_prefix}"#{milestone.name.reverse}") + + expect(reference_filter(act).to_html).to eq exp + end + end + + describe 'referencing a milestone in a link href' do + let(:reference) { %Q{<a href="#{milestone.to_reference}">Milestone</a>} } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(project.namespace, project, milestone) + end + + it 'links with adjacent text' do + doc = reference_filter("Milestone (#{reference}.)") + expect(doc.to_html).to match(%r(\(<a.+>Milestone</a>\.\))) end it 'includes a data-project attribute' do - doc = reference_filter("milestone #{reference}") + doc = reference_filter("Milestone #{reference}") link = doc.css('a').first expect(link).to have_attribute('data-project') @@ -66,10 +146,31 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do expect(link).to have_attribute('data-milestone') expect(link.attr('data-milestone')).to eq milestone.id.to_s end + end + + describe 'cross project milestone references' do + let(:another_project) { create(:empty_project, :public) } + let(:project_path) { another_project.path_with_namespace } + let(:milestone) { create(:milestone, project: another_project) } + let(:reference) { milestone.to_reference(project) } + + let!(:result) { reference_filter("See #{reference}") } + + it 'points to referenced project milestone page' do + expect(result.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(another_project.namespace, + another_project, + milestone) + end - it 'adds to the results hash' do - result = reference_pipeline_result("milestone #{reference}") - expect(result[:references][:milestone]).to eq [milestone] + it 'contains cross project content' do + expect(result.css('a').first.text).to eq "#{milestone.name} in #{project_path}" + end + + it 'escapes the name attribute' do + allow_any_instance_of(Milestone).to receive(:title).and_return(%{"></a>whatever<a title="}) + doc = reference_filter("See #{reference}") + expect(doc.css('a').first.text).to eq "#{milestone.name} in #{project_path}" end end end diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb index c2c2fd0eb6a..697d10bbf70 100644 --- a/spec/lib/banzai/filter/redactor_filter_spec.rb +++ b/spec/lib/banzai/filter/redactor_filter_spec.rb @@ -16,11 +16,23 @@ describe Banzai::Filter::RedactorFilter, lib: true do end context 'with data-project' do + let(:parser_class) do + Class.new(Banzai::ReferenceParser::BaseParser) do + self.reference_type = :test + end + end + + before do + allow(Banzai::ReferenceParser).to receive(:[]). + with('test'). + and_return(parser_class) + end + it 'removes unpermitted Project references' do user = create(:user) project = create(:empty_project) - link = reference_link(project: project.id, reference_filter: 'ReferenceFilter') + link = reference_link(project: project.id, reference_type: 'test') doc = filter(link, current_user: user) expect(doc.css('a').length).to eq 0 @@ -31,14 +43,14 @@ describe Banzai::Filter::RedactorFilter, lib: true do project = create(:empty_project) project.team << [user, :master] - link = reference_link(project: project.id, reference_filter: 'ReferenceFilter') + link = reference_link(project: project.id, reference_type: 'test') doc = filter(link, current_user: user) expect(doc.css('a').length).to eq 1 end it 'handles invalid Project references' do - link = reference_link(project: 12345, reference_filter: 'ReferenceFilter') + link = reference_link(project: 12345, reference_type: 'test') expect { filter(link) }.not_to raise_error end @@ -51,7 +63,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do project = create(:empty_project, :public) issue = create(:issue, :confidential, project: project) - link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue') doc = filter(link, current_user: non_member) expect(doc.css('a').length).to eq 0 @@ -62,7 +74,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do project = create(:empty_project, :public) issue = create(:issue, :confidential, project: project, author: author) - link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue') doc = filter(link, current_user: author) expect(doc.css('a').length).to eq 1 @@ -73,7 +85,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do project = create(:empty_project, :public) issue = create(:issue, :confidential, project: project, assignee: assignee) - link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue') doc = filter(link, current_user: assignee) expect(doc.css('a').length).to eq 1 @@ -85,7 +97,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do project.team << [member, :developer] issue = create(:issue, :confidential, project: project) - link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue') doc = filter(link, current_user: member) expect(doc.css('a').length).to eq 1 @@ -96,7 +108,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do project = create(:empty_project, :public) issue = create(:issue, :confidential, project: project) - link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue') doc = filter(link, current_user: admin) expect(doc.css('a').length).to eq 1 @@ -108,7 +120,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do project = create(:empty_project, :public) issue = create(:issue, project: project) - link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue') doc = filter(link, current_user: user) expect(doc.css('a').length).to eq 1 @@ -121,7 +133,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do user = create(:user) group = create(:group, :private) - link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter') + link = reference_link(group: group.id, reference_type: 'user') doc = filter(link, current_user: user) expect(doc.css('a').length).to eq 0 @@ -132,14 +144,14 @@ describe Banzai::Filter::RedactorFilter, lib: true do group = create(:group, :private) group.add_developer(user) - link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter') + link = reference_link(group: group.id, reference_type: 'user') doc = filter(link, current_user: user) expect(doc.css('a').length).to eq 1 end it 'handles invalid Group references' do - link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter') + link = reference_link(group: 12345, reference_type: 'user') expect { filter(link) }.not_to raise_error end @@ -149,7 +161,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do it 'allows any User reference' do user = create(:user) - link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter') + link = reference_link(user: user.id, reference_type: 'user') doc = filter(link) expect(doc.css('a').length).to eq 1 diff --git a/spec/lib/banzai/filter/reference_filter_spec.rb b/spec/lib/banzai/filter/reference_filter_spec.rb new file mode 100644 index 00000000000..55e681f6faf --- /dev/null +++ b/spec/lib/banzai/filter/reference_filter_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Banzai::Filter::ReferenceFilter, lib: true do + let(:project) { build(:project) } + + describe '#each_node' do + it 'iterates over the nodes in a document' do + document = Nokogiri::HTML.fragment('<a href="foo">foo</a>') + filter = described_class.new(document, project: project) + + expect { |b| filter.each_node(&b) }. + to yield_with_args(an_instance_of(Nokogiri::XML::Element)) + end + + it 'returns an Enumerator when no block is given' do + document = Nokogiri::HTML.fragment('<a href="foo">foo</a>') + filter = described_class.new(document, project: project) + + expect(filter.each_node).to be_an_instance_of(Enumerator) + end + + it 'skips links with a "gfm" class' do + document = Nokogiri::HTML.fragment('<a href="foo" class="gfm">foo</a>') + filter = described_class.new(document, project: project) + + expect { |b| filter.each_node(&b) }.not_to yield_control + end + + it 'skips text nodes in pre elements' do + document = Nokogiri::HTML.fragment('<pre>foo</pre>') + filter = described_class.new(document, project: project) + + expect { |b| filter.each_node(&b) }.not_to yield_control + end + end + + describe '#nodes' do + it 'returns an Array of the HTML nodes' do + document = Nokogiri::HTML.fragment('<a href="foo">foo</a>') + filter = described_class.new(document, project: project) + + expect(filter.nodes).to eq([document.children[0]]) + end + end +end diff --git a/spec/lib/banzai/filter/reference_gatherer_filter_spec.rb b/spec/lib/banzai/filter/reference_gatherer_filter_spec.rb deleted file mode 100644 index c8b1dfdf944..00000000000 --- a/spec/lib/banzai/filter/reference_gatherer_filter_spec.rb +++ /dev/null @@ -1,87 +0,0 @@ -require 'spec_helper' - -describe Banzai::Filter::ReferenceGathererFilter, lib: true do - include ActionView::Helpers::UrlHelper - include FilterSpecHelper - - def reference_link(data) - link_to('text', '', class: 'gfm', data: data) - end - - context "for issue references" do - - context 'with data-project' do - it 'removes unpermitted Project references' do - user = create(:user) - project = create(:empty_project) - issue = create(:issue, project: project) - - link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') - result = pipeline_result(link, current_user: user) - - expect(result[:references][:issue]).to be_empty - end - - it 'allows permitted Project references' do - user = create(:user) - project = create(:empty_project) - issue = create(:issue, project: project) - project.team << [user, :master] - - link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') - result = pipeline_result(link, current_user: user) - - expect(result[:references][:issue]).to eq([issue]) - end - - it 'handles invalid Project references' do - link = reference_link(project: 12345, issue: 12345, reference_filter: 'IssueReferenceFilter') - - expect { pipeline_result(link) }.not_to raise_error - end - end - end - - context "for user references" do - - context 'with data-group' do - it 'removes unpermitted Group references' do - user = create(:user) - group = create(:group) - - link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter') - result = pipeline_result(link, current_user: user) - - expect(result[:references][:user]).to be_empty - end - - it 'allows permitted Group references' do - user = create(:user) - group = create(:group) - group.add_developer(user) - - link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter') - result = pipeline_result(link, current_user: user) - - expect(result[:references][:user]).to eq([user]) - end - - it 'handles invalid Group references' do - link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter') - - expect { pipeline_result(link) }.not_to raise_error - end - end - - context 'with data-user' do - it 'allows any User reference' do - user = create(:user) - - link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter') - result = pipeline_result(link) - - expect(result[:references][:user]).to eq([user]) - end - end - end -end diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index 27ce312b11c..b38e3b17e64 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -22,6 +22,12 @@ describe Banzai::Filter::SanitizationFilter, lib: true do expect(filter(act).to_html).to eq exp end + it 'sanitizes mixed-cased javascript in attributes' do + act = %q(<a href="javaScript:alert('foo')">Text</a>) + exp = '<a>Text</a>' + expect(filter(act).to_html).to eq exp + end + it 'allows whitelisted HTML tags from the user' do exp = act = "<dl>\n<dt>Term</dt>\n<dd>Definition</dd>\n</dl>" expect(filter(act).to_html).to eq exp diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb index 26466fbb180..5068ddd7faa 100644 --- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb @@ -77,11 +77,6 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do expect(link).not_to match %r(https?://) expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true) end - - it 'adds to the results hash' do - result = reference_pipeline_result("Snippet #{reference}") - expect(result[:references][:snippet]).to eq [snippet] - end end context 'cross-project reference' do @@ -107,11 +102,6 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do expect(reference_filter(act).to_html).to eq exp end - - it 'adds to the results hash' do - result = reference_pipeline_result("Snippet #{reference}") - expect(result[:references][:snippet]).to eq [snippet] - end end context 'cross-project URL reference' do @@ -137,10 +127,5 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/) end - - it 'adds to the results hash' do - result = reference_pipeline_result("Snippet #{reference}") - expect(result[:references][:snippet]).to eq [snippet] - end end end diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb index 3b073a90a95..b83be54746c 100644 --- a/spec/lib/banzai/filter/upload_link_filter_spec.rb +++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb @@ -8,6 +8,10 @@ describe Banzai::Filter::UploadLinkFilter, lib: true do project: project }) + raw_filter(doc, contexts) + end + + def raw_filter(doc, contexts = {}) described_class.call(doc, contexts) end @@ -70,4 +74,18 @@ describe Banzai::Filter::UploadLinkFilter, lib: true do expect(doc.at_css('img')['src']).to match "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/%ED%95%9C%EA%B8%80.png" end end + + context 'when project context does not exist' do + let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') } + + it 'does not raise error' do + expect { raw_filter(upload_link, project: nil) }.not_to raise_error + end + + it 'does not rewrite link' do + doc = raw_filter(upload_link, project: nil) + + expect(doc.to_html).to eq upload_link + end + end end diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 8bdebae1841..108b36a97cc 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -31,28 +31,22 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do end it 'supports a special @all mention' do - doc = reference_filter("Hey #{reference}") + doc = reference_filter("Hey #{reference}", author: user) expect(doc.css('a').length).to eq 1 expect(doc.css('a').first.attr('href')) .to eq urls.namespace_project_url(project.namespace, project) end - context "when the author is a member of the project" do + it 'includes a data-author attribute when there is an author' do + doc = reference_filter(reference, author: user) - it 'adds to the results hash' do - result = reference_pipeline_result("Hey #{reference}", author: project.creator) - expect(result[:references][:user]).to eq [project.creator] - end + expect(doc.css('a').first.attr('data-author')).to eq(user.id.to_s) end - context "when the author is not a member of the project" do - - let(:other_user) { create(:user) } + it 'does not include a data-author attribute when there is no author' do + doc = reference_filter(reference) - it "doesn't add to the results hash" do - result = reference_pipeline_result("Hey #{reference}", author: other_user) - expect(result[:references][:user]).to eq [] - end + expect(doc.css('a').first.has_attribute?('data-author')).to eq(false) end end @@ -83,11 +77,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do expect(link).to have_attribute('data-user') expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s end - - it 'adds to the results hash' do - result = reference_pipeline_result("Hey #{reference}") - expect(result[:references][:user]).to eq [user] - end end context 'mentioning a group' do @@ -106,11 +95,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do expect(link).to have_attribute('data-group') expect(link.attr('data-group')).to eq group.id.to_s end - - it 'adds to the results hash' do - result = reference_pipeline_result("Hey #{reference}") - expect(result[:references][:user]).to eq group.users - end end it 'links with adjacent text' do @@ -151,10 +135,24 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do expect(link).to have_attribute('data-user') expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s end + end + + describe '#namespaces' do + it 'returns a Hash containing all Namespaces' do + document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>") + filter = described_class.new(document, project: project) + ns = user.namespace + + expect(filter.namespaces).to eq({ ns.path => ns }) + end + end + + describe '#usernames' do + it 'returns the usernames mentioned in a document' do + document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>") + filter = described_class.new(document, project: project) - it 'adds to the results hash' do - result = reference_pipeline_result("Hey #{reference}") - expect(result[:references][:user]).to eq [user] + expect(filter.usernames).to eq([user.username]) end end end diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb index 7aa1b4a3bf6..72bc6a0b704 100644 --- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -50,4 +50,112 @@ describe Banzai::Pipeline::WikiPipeline do end end end + + describe "Links" do + let(:namespace) { create(:namespace, name: "wiki_link_ns") } + let(:project) { create(:empty_project, :public, name: "wiki_link_project", namespace: namespace) } + let(:project_wiki) { ProjectWiki.new(project, double(:user)) } + let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) } + + { "when GitLab is hosted at a root URL" => '/', + "when GitLab is hosted at a relative URL" => '/nested/relative/gitlab' }.each do |test_name, relative_url_root| + + context test_name do + before do + allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return(relative_url_root) + end + + describe "linking to pages within the wiki" do + context "when creating hierarchical links to the current directory" do + it "rewrites non-file links to be at the scope of the current directory" do + markdown = "[Page](./page)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page\"") + end + + it "rewrites file links to be at the scope of the current directory" do + markdown = "[Link to Page](./page.md)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"") + end + end + + context "when creating hierarchical links to the parent directory" do + it "rewrites non-file links to be at the scope of the parent directory" do + markdown = "[Link to Page](../page)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/page\"") + end + + it "rewrites file links to be at the scope of the parent directory" do + markdown = "[Link to Page](../page.md)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/page.md\"") + end + end + + context "when creating hierarchical links to a sub-directory" do + it "rewrites non-file links to be at the scope of the sub-directory" do + markdown = "[Link to Page](./subdirectory/page)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/subdirectory/page\"") + end + + it "rewrites file links to be at the scope of the sub-directory" do + markdown = "[Link to Page](./subdirectory/page.md)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/subdirectory/page.md\"") + end + end + + describe "when creating non-hierarchical links" do + it 'rewrites non-file links to be at the scope of the wiki root' do + markdown = "[Link to Page](page)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"") + end + + it "rewrites file links to be at the scope of the current directory" do + markdown = "[Link to Page](page.md)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"") + end + end + + describe "when creating root links" do + it 'rewrites non-file links to be at the scope of the wiki root' do + markdown = "[Link to Page](/page)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"") + end + + it 'rewrites file links to be at the scope of the wiki root' do + markdown = "[Link to Page](/page.md)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page.md\"") + end + end + end + + describe "linking to pages outside the wiki (absolute)" do + it "doesn't rewrite links" do + markdown = "[Link to Page](http://example.com/page)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include('href="http://example.com/page"') + end + end + end + end + end end diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb new file mode 100644 index 00000000000..543b4786d84 --- /dev/null +++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb @@ -0,0 +1,237 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::BaseParser, lib: true do + include ReferenceParserHelpers + + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + + subject do + klass = Class.new(described_class) do + self.reference_type = :foo + end + + klass.new(project, user) + end + + describe '.reference_type=' do + it 'sets the reference type' do + dummy = Class.new(described_class) + dummy.reference_type = :foo + + expect(dummy.reference_type).to eq(:foo) + end + end + + describe '#nodes_visible_to_user' do + let(:link) { empty_html_link } + + context 'when the link has a data-project attribute' do + it 'returns the nodes if the attribute value equals the current project ID' do + link['data-project'] = project.id.to_s + + expect(Ability.abilities).not_to receive(:allowed?) + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + + it 'returns the nodes if the user can read the project' do + other_project = create(:empty_project, :public) + + link['data-project'] = other_project.id.to_s + + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_project, other_project). + and_return(true) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + + it 'returns an empty Array when the attribute value is empty' do + link['data-project'] = '' + + expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + end + + it 'returns an empty Array when the user can not read the project' do + other_project = create(:empty_project, :public) + + link['data-project'] = other_project.id.to_s + + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_project, other_project). + and_return(false) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + end + end + + context 'when the link does not have a data-project attribute' do + it 'returns the nodes' do + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + end + end + + describe '#nodes_user_can_reference' do + it 'returns the nodes' do + link = double(:link) + + expect(subject.nodes_user_can_reference(user, [link])).to eq([link]) + end + end + + describe '#referenced_by' do + context 'when references_relation is implemented' do + it 'returns a collection of objects' do + links = Nokogiri::HTML.fragment("<a data-foo='#{user.id}'></a>"). + children + + expect(subject).to receive(:references_relation).and_return(User) + expect(subject.referenced_by(links)).to eq([user]) + end + end + + context 'when references_relation is not implemented' do + it 'raises NotImplementedError' do + links = Nokogiri::HTML.fragment('<a data-foo="1"></a>').children + + expect { subject.referenced_by(links) }. + to raise_error(NotImplementedError) + end + end + end + + describe '#references_relation' do + it 'raises NotImplementedError' do + expect { subject.references_relation }.to raise_error(NotImplementedError) + end + end + + describe '#gather_attributes_per_project' do + it 'returns a Hash containing attribute values per project' do + link = Nokogiri::HTML.fragment('<a data-project="1" data-foo="2"></a>'). + children[0] + + hash = subject.gather_attributes_per_project([link], 'data-foo') + + expect(hash).to be_an_instance_of(Hash) + + expect(hash[1].to_a).to eq(['2']) + end + end + + describe '#grouped_objects_for_nodes' do + it 'returns a Hash grouping objects per ID' do + nodes = [double(:node)] + + expect(subject).to receive(:unique_attribute_values). + with(nodes, 'data-user'). + and_return([user.id]) + + hash = subject.grouped_objects_for_nodes(nodes, User, 'data-user') + + expect(hash).to eq({ user.id => user }) + end + + it 'returns an empty Hash when the list of nodes is empty' do + expect(subject.grouped_objects_for_nodes([], User, 'data-user')).to eq({}) + end + end + + describe '#unique_attribute_values' do + it 'returns an Array of unique values' do + link = double(:link) + + expect(link).to receive(:has_attribute?). + with('data-foo'). + twice. + and_return(true) + + expect(link).to receive(:attr). + with('data-foo'). + twice. + and_return('1') + + nodes = [link, link] + + expect(subject.unique_attribute_values(nodes, 'data-foo')).to eq(['1']) + end + end + + describe '#process' do + it 'gathers the references for every node matching the reference type' do + dummy = Class.new(described_class) do + self.reference_type = :test + end + + instance = dummy.new(project, user) + document = Nokogiri::HTML.fragment('<a class="gfm"></a><a class="gfm" data-reference-type="test"></a>') + + expect(instance).to receive(:gather_references). + with([document.children[1]]). + and_return([user]) + + expect(instance.process([document])).to eq([user]) + end + end + + describe '#gather_references' do + let(:link) { double(:link) } + + it 'does not process links a user can not reference' do + expect(subject).to receive(:nodes_user_can_reference). + with(user, [link]). + and_return([]) + + expect(subject).to receive(:referenced_by).with([]) + + subject.gather_references([link]) + end + + it 'does not process links a user can not see' do + expect(subject).to receive(:nodes_user_can_reference). + with(user, [link]). + and_return([link]) + + expect(subject).to receive(:nodes_visible_to_user). + with(user, [link]). + and_return([]) + + expect(subject).to receive(:referenced_by).with([]) + + subject.gather_references([link]) + end + + it 'returns the references if a user can reference and see a link' do + expect(subject).to receive(:nodes_user_can_reference). + with(user, [link]). + and_return([link]) + + expect(subject).to receive(:nodes_visible_to_user). + with(user, [link]). + and_return([link]) + + expect(subject).to receive(:referenced_by).with([link]) + + subject.gather_references([link]) + end + end + + describe '#can?' do + it 'delegates the permissions check to the Ability class' do + user = double(:user) + + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_project, project) + + subject.can?(user, :read_project, project) + end + end + + describe '#find_projects_for_hash_keys' do + it 'returns a list of Projects' do + expect(subject.find_projects_for_hash_keys(project.id => project)). + to eq([project]) + end + end +end diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb new file mode 100644 index 00000000000..0b76d29fce0 --- /dev/null +++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb @@ -0,0 +1,113 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::CommitParser, lib: true do + include ReferenceParserHelpers + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + subject { described_class.new(project, user) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + context 'when the link has a data-project attribute' do + before do + link['data-project'] = project.id.to_s + end + + context 'when the link has a data-commit attribute' do + before do + link['data-commit'] = '123' + end + + it 'returns an Array of commits' do + commit = double(:commit) + + allow_any_instance_of(Project).to receive(:valid_repo?). + and_return(true) + + expect(subject).to receive(:find_commits). + with(project, ['123']). + and_return([commit]) + + expect(subject.referenced_by([link])).to eq([commit]) + end + + it 'returns an empty Array when the commit could not be found' do + allow_any_instance_of(Project).to receive(:valid_repo?). + and_return(true) + + expect(subject).to receive(:find_commits). + with(project, ['123']). + and_return([]) + + expect(subject.referenced_by([link])).to eq([]) + end + + it 'skips projects without valid repositories' do + allow_any_instance_of(Project).to receive(:valid_repo?). + and_return(false) + + expect(subject.referenced_by([link])).to eq([]) + end + end + + context 'when the link does not have a data-commit attribute' do + it 'returns an empty Array' do + allow_any_instance_of(Project).to receive(:valid_repo?). + and_return(true) + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + + context 'when the link does not have a data-project attribute' do + it 'returns an empty Array' do + allow_any_instance_of(Project).to receive(:valid_repo?). + and_return(true) + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + + describe '#commit_ids_per_project' do + before do + link['data-project'] = project.id.to_s + end + + it 'returns a Hash containing commit IDs per project' do + link['data-commit'] = '123' + + hash = subject.commit_ids_per_project([link]) + + expect(hash).to be_an_instance_of(Hash) + + expect(hash[project.id].to_a).to eq(['123']) + end + + it 'does not add a project when the data-commit attribute is empty' do + hash = subject.commit_ids_per_project([link]) + + expect(hash).to be_empty + end + end + + describe '#find_commits' do + it 'returns an Array of commit objects' do + commit = double(:commit) + + expect(project).to receive(:commit).with('123').and_return(commit) + expect(project).to receive(:valid_repo?).and_return(true) + + expect(subject.find_commits(project, %w{123})).to eq([commit]) + end + + it 'skips commit IDs for which no commit could be found' do + expect(project).to receive(:commit).with('123').and_return(nil) + expect(project).to receive(:valid_repo?).and_return(true) + + expect(subject.find_commits(project, %w{123})).to eq([]) + end + end +end diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb new file mode 100644 index 00000000000..ba982f38542 --- /dev/null +++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::CommitRangeParser, lib: true do + include ReferenceParserHelpers + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + subject { described_class.new(project, user) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + context 'when the link has a data-project attribute' do + before do + link['data-project'] = project.id.to_s + end + + context 'when the link as a data-commit-range attribute' do + before do + link['data-commit-range'] = '123..456' + end + + it 'returns an Array of commit ranges' do + range = double(:range) + + expect(subject).to receive(:find_object). + with(project, '123..456'). + and_return(range) + + expect(subject.referenced_by([link])).to eq([range]) + end + + it 'returns an empty Array when the commit range could not be found' do + expect(subject).to receive(:find_object). + with(project, '123..456'). + and_return(nil) + + expect(subject.referenced_by([link])).to eq([]) + end + end + + context 'when the link does not have a data-commit-range attribute' do + it 'returns an empty Array' do + expect(subject.referenced_by([link])).to eq([]) + end + end + end + + context 'when the link does not have a data-project attribute' do + it 'returns an empty Array' do + expect(subject.referenced_by([link])).to eq([]) + end + end + end + + describe '#commit_range_ids_per_project' do + before do + link['data-project'] = project.id.to_s + end + + it 'returns a Hash containing range IDs per project' do + link['data-commit-range'] = '123..456' + + hash = subject.commit_range_ids_per_project([link]) + + expect(hash).to be_an_instance_of(Hash) + + expect(hash[project.id].to_a).to eq(['123..456']) + end + + it 'does not add a project when the data-commit-range attribute is empty' do + hash = subject.commit_range_ids_per_project([link]) + + expect(hash).to be_empty + end + end + + describe '#find_ranges' do + it 'returns an Array of range objects' do + range = double(:commit) + + expect(subject).to receive(:find_object). + with(project, '123..456'). + and_return(range) + + expect(subject.find_ranges(project, ['123..456'])).to eq([range]) + end + + it 'skips ranges that could not be found' do + expect(subject).to receive(:find_object). + with(project, '123..456'). + and_return(nil) + + expect(subject.find_ranges(project, ['123..456'])).to eq([]) + end + end + + describe '#find_object' do + let(:range) { double(:range) } + + before do + expect(CommitRange).to receive(:new).and_return(range) + end + + context 'when the range has valid commits' do + it 'returns the commit range' do + expect(range).to receive(:valid_commits?).and_return(true) + + expect(subject.find_object(project, '123..456')).to eq(range) + end + end + + context 'when the range does not have any valid commits' do + it 'returns nil' do + expect(range).to receive(:valid_commits?).and_return(false) + + expect(subject.find_object(project, '123..456')).to be_nil + end + end + end +end diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb new file mode 100644 index 00000000000..a6ef8394fe7 --- /dev/null +++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do + include ReferenceParserHelpers + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + subject { described_class.new(project, user) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + context 'when the link has a data-project attribute' do + before do + link['data-project'] = project.id.to_s + end + + context 'when the link has a data-external-issue attribute' do + it 'returns an Array of ExternalIssue instances' do + link['data-external-issue'] = '123' + + refs = subject.referenced_by([link]) + + expect(refs).to eq([ExternalIssue.new('123', project)]) + end + end + + context 'when the link does not have a data-external-issue attribute' do + it 'returns an empty Array' do + expect(subject.referenced_by([link])).to eq([]) + end + end + end + + context 'when the link does not have a data-project attribute' do + it 'returns an empty Array' do + expect(subject.referenced_by([link])).to eq([]) + end + end + end + + describe '#issue_ids_per_project' do + before do + link['data-project'] = project.id.to_s + end + + it 'returns a Hash containing range IDs per project' do + link['data-external-issue'] = '123' + + hash = subject.issue_ids_per_project([link]) + + expect(hash).to be_an_instance_of(Hash) + + expect(hash[project.id].to_a).to eq(['123']) + end + + it 'does not add a project when the data-external-issue attribute is empty' do + hash = subject.issue_ids_per_project([link]) + + expect(hash).to be_empty + end + end +end diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb new file mode 100644 index 00000000000..514c752546d --- /dev/null +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::IssueParser, lib: true do + include ReferenceParserHelpers + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + let(:issue) { create(:issue, project: project) } + subject { described_class.new(project, user) } + let(:link) { empty_html_link } + + describe '#nodes_visible_to_user' do + context 'when the link has a data-issue attribute' do + before do + link['data-issue'] = issue.id.to_s + end + + it 'returns the nodes when the user can read the issue' do + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_issue, issue). + and_return(true) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + + it 'returns an empty Array when the user can not read the issue' do + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_issue, issue). + and_return(false) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + end + end + + context 'when the link does not have a data-issue attribute' do + it 'returns an empty Array' do + expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + end + end + + context 'when the project uses an external issue tracker' do + it 'returns all nodes' do + link = double(:link) + + expect(project).to receive(:external_issue_tracker).and_return(true) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + end + end + + describe '#referenced_by' do + context 'when the link has a data-issue attribute' do + context 'using an existing issue ID' do + before do + link['data-issue'] = issue.id.to_s + end + + it 'returns an Array of issues' do + expect(subject.referenced_by([link])).to eq([issue]) + end + + it 'returns an empty Array when the list of nodes is empty' do + expect(subject.referenced_by([link])).to eq([issue]) + expect(subject.referenced_by([])).to eq([]) + end + end + end + end + + describe '#issues_for_nodes' do + it 'returns a Hash containing the issues for a list of nodes' do + link['data-issue'] = issue.id.to_s + nodes = [link] + + expect(subject.issues_for_nodes(nodes)).to eq({ issue.id => issue }) + end + end +end diff --git a/spec/lib/banzai/reference_parser/label_parser_spec.rb b/spec/lib/banzai/reference_parser/label_parser_spec.rb new file mode 100644 index 00000000000..77fda47f0e7 --- /dev/null +++ b/spec/lib/banzai/reference_parser/label_parser_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::LabelParser, lib: true do + include ReferenceParserHelpers + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + let(:label) { create(:label, project: project) } + subject { described_class.new(project, user) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + describe 'when the link has a data-label attribute' do + context 'using an existing label ID' do + it 'returns an Array of labels' do + link['data-label'] = label.id.to_s + + expect(subject.referenced_by([link])).to eq([label]) + end + end + + context 'using a non-existing label ID' do + it 'returns an empty Array' do + link['data-label'] = '' + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + end +end diff --git a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb new file mode 100644 index 00000000000..cf89ad598ea --- /dev/null +++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::MergeRequestParser, lib: true do + include ReferenceParserHelpers + + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request) } + subject { described_class.new(merge_request.target_project, user) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + describe 'when the link has a data-merge-request attribute' do + context 'using an existing merge request ID' do + it 'returns an Array of merge requests' do + link['data-merge-request'] = merge_request.id.to_s + + expect(subject.referenced_by([link])).to eq([merge_request]) + end + end + + context 'using a non-existing merge request ID' do + it 'returns an empty Array' do + link['data-merge-request'] = '' + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + end +end diff --git a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb new file mode 100644 index 00000000000..6aa45a22cc4 --- /dev/null +++ b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::MilestoneParser, lib: true do + include ReferenceParserHelpers + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + let(:milestone) { create(:milestone, project: project) } + subject { described_class.new(project, user) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + describe 'when the link has a data-milestone attribute' do + context 'using an existing milestone ID' do + it 'returns an Array of milestones' do + link['data-milestone'] = milestone.id.to_s + + expect(subject.referenced_by([link])).to eq([milestone]) + end + end + + context 'using a non-existing milestone ID' do + it 'returns an empty Array' do + link['data-milestone'] = '' + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + end +end diff --git a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb new file mode 100644 index 00000000000..59127b7c5d1 --- /dev/null +++ b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::SnippetParser, lib: true do + include ReferenceParserHelpers + + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + let(:snippet) { create(:snippet, project: project) } + subject { described_class.new(project, user) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + describe 'when the link has a data-snippet attribute' do + context 'using an existing snippet ID' do + it 'returns an Array of snippets' do + link['data-snippet'] = snippet.id.to_s + + expect(subject.referenced_by([link])).to eq([snippet]) + end + end + + context 'using a non-existing snippet ID' do + it 'returns an empty Array' do + link['data-snippet'] = '' + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + end +end diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb new file mode 100644 index 00000000000..9a82891297d --- /dev/null +++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb @@ -0,0 +1,189 @@ +require 'spec_helper' + +describe Banzai::ReferenceParser::UserParser, lib: true do + include ReferenceParserHelpers + + let(:group) { create(:group) } + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public, group: group, creator: user) } + subject { described_class.new(project, user) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + context 'when the link has a data-group attribute' do + context 'using an existing group ID' do + before do + link['data-group'] = project.group.id.to_s + end + + it 'returns the users of the group' do + create(:group_member, group: group, user: user) + + expect(subject.referenced_by([link])).to eq([user]) + end + + it 'returns an empty Array when the group has no users' do + expect(subject.referenced_by([link])).to eq([]) + end + end + + context 'using a non-existing group ID' do + it 'returns an empty Array' do + link['data-group'] = '' + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + + context 'when the link has a data-user attribute' do + it 'returns an Array of users' do + link['data-user'] = user.id.to_s + + expect(subject.referenced_by([link])).to eq([user]) + end + end + + context 'when the link has a data-project attribute' do + context 'using an existing project ID' do + let(:contributor) { create(:user) } + + before do + project.team << [user, :developer] + project.team << [contributor, :developer] + end + + it 'returns the members of a project' do + link['data-project'] = project.id.to_s + + # This uses an explicit sort to make sure this spec doesn't randomly + # fail when objects are returned in a different order. + refs = subject.referenced_by([link]).sort_by(&:id) + + expect(refs).to eq([user, contributor]) + end + end + + context 'using a non-existing project ID' do + it 'returns an empty Array' do + link['data-project'] = '' + + expect(subject.referenced_by([link])).to eq([]) + end + end + end + end + + describe '#nodes_visible_to_use?' do + context 'when the link has a data-group attribute' do + context 'using an existing group ID' do + before do + link['data-group'] = group.id.to_s + end + + it 'returns the nodes if the user can read the group' do + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_group, group). + and_return(true) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + + it 'returns an empty Array if the user can not read the group' do + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_group, group). + and_return(false) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + end + end + + context 'when the link does not have a data-group attribute' do + context 'with a data-project attribute' do + it 'returns the nodes if the attribute value equals the current project ID' do + link['data-project'] = project.id.to_s + + expect(Ability.abilities).not_to receive(:allowed?) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + + it 'returns the nodes if the user can read the project' do + other_project = create(:empty_project, :public) + + link['data-project'] = other_project.id.to_s + + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_project, other_project). + and_return(true) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + + it 'returns an empty Array if the user can not read the project' do + other_project = create(:empty_project, :public) + + link['data-project'] = other_project.id.to_s + + expect(Ability.abilities).to receive(:allowed?). + with(user, :read_project, other_project). + and_return(false) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + end + end + + context 'without a data-project attribute' do + it 'returns the nodes' do + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + end + end + end + end + + describe '#nodes_user_can_reference' do + context 'when the link has a data-author attribute' do + it 'returns the nodes when the user is a member of the project' do + other_project = create(:project) + other_project.team << [user, :developer] + + link['data-project'] = other_project.id.to_s + link['data-author'] = user.id.to_s + + expect(subject.nodes_user_can_reference(user, [link])).to eq([link]) + end + + it 'returns an empty Array when the project could not be found' do + link['data-project'] = '' + link['data-author'] = user.id.to_s + + expect(subject.nodes_user_can_reference(user, [link])).to eq([]) + end + + it 'returns an empty Array when the user could not be found' do + other_project = create(:project) + + link['data-project'] = other_project.id.to_s + link['data-author'] = '' + + expect(subject.nodes_user_can_reference(user, [link])).to eq([]) + end + + it 'returns an empty Array when the user is not a team member' do + other_project = create(:project) + + link['data-project'] = other_project.id.to_s + link['data-author'] = user.id.to_s + + expect(subject.nodes_user_can_reference(user, [link])).to eq([]) + end + end + + context 'when the link does not have a data-author attribute' do + it 'returns the nodes' do + expect(subject.nodes_user_can_reference(user, [link])).to eq([link]) + end + end + end +end diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb index 3a2b568f4c7..898f1e84ab0 100644 --- a/spec/lib/ci/ansi2html_spec.rb +++ b/spec/lib/ci/ansi2html_spec.rb @@ -4,131 +4,185 @@ describe Ci::Ansi2html, lib: true do subject { Ci::Ansi2html } it "prints non-ansi as-is" do - expect(subject.convert("Hello")).to eq('Hello') + expect(subject.convert("Hello")[:html]).to eq('Hello') end it "strips non-color-changing controll sequences" do - expect(subject.convert("Hello \e[2Kworld")).to eq('Hello world') + expect(subject.convert("Hello \e[2Kworld")[:html]).to eq('Hello world') end it "prints simply red" do - expect(subject.convert("\e[31mHello\e[0m")).to eq('<span class="term-fg-red">Hello</span>') + expect(subject.convert("\e[31mHello\e[0m")[:html]).to eq('<span class="term-fg-red">Hello</span>') end it "prints simply red without trailing reset" do - expect(subject.convert("\e[31mHello")).to eq('<span class="term-fg-red">Hello</span>') + expect(subject.convert("\e[31mHello")[:html]).to eq('<span class="term-fg-red">Hello</span>') end it "prints simply yellow" do - expect(subject.convert("\e[33mHello\e[0m")).to eq('<span class="term-fg-yellow">Hello</span>') + expect(subject.convert("\e[33mHello\e[0m")[:html]).to eq('<span class="term-fg-yellow">Hello</span>') end it "prints default on blue" do - expect(subject.convert("\e[39;44mHello")).to eq('<span class="term-bg-blue">Hello</span>') + expect(subject.convert("\e[39;44mHello")[:html]).to eq('<span class="term-bg-blue">Hello</span>') end it "prints red on blue" do - expect(subject.convert("\e[31;44mHello")).to eq('<span class="term-fg-red term-bg-blue">Hello</span>') + expect(subject.convert("\e[31;44mHello")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello</span>') end it "resets colors after red on blue" do - expect(subject.convert("\e[31;44mHello\e[0m world")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> world') + expect(subject.convert("\e[31;44mHello\e[0m world")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello</span> world') end it "performs color change from red/blue to yellow/blue" do - expect(subject.convert("\e[31;44mHello \e[33mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-blue">world</span>') + expect(subject.convert("\e[31;44mHello \e[33mworld")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-blue">world</span>') end it "performs color change from red/blue to yellow/green" do - expect(subject.convert("\e[31;44mHello \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-green">world</span>') + expect(subject.convert("\e[31;44mHello \e[33;42mworld")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-green">world</span>') end it "performs color change from red/blue to reset to yellow/green" do - expect(subject.convert("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> <span class="term-fg-yellow term-bg-green">world</span>') + expect(subject.convert("\e[31;44mHello\e[0m \e[33;42mworld")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello</span> <span class="term-fg-yellow term-bg-green">world</span>') end it "ignores unsupported codes" do - expect(subject.convert("\e[51mHello\e[0m")).to eq('Hello') + expect(subject.convert("\e[51mHello\e[0m")[:html]).to eq('Hello') end it "prints light red" do - expect(subject.convert("\e[91mHello\e[0m")).to eq('<span class="term-fg-l-red">Hello</span>') + expect(subject.convert("\e[91mHello\e[0m")[:html]).to eq('<span class="term-fg-l-red">Hello</span>') end it "prints default on light red" do - expect(subject.convert("\e[101mHello\e[0m")).to eq('<span class="term-bg-l-red">Hello</span>') + expect(subject.convert("\e[101mHello\e[0m")[:html]).to eq('<span class="term-bg-l-red">Hello</span>') end it "performs color change from red/blue to default/blue" do - expect(subject.convert("\e[31;44mHello \e[39mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>') + expect(subject.convert("\e[31;44mHello \e[39mworld")[:html]).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>') end it "performs color change from light red/blue to default/blue" do - expect(subject.convert("\e[91;44mHello \e[39mworld")).to eq('<span class="term-fg-l-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>') + expect(subject.convert("\e[91;44mHello \e[39mworld")[:html]).to eq('<span class="term-fg-l-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>') end it "prints bold text" do - expect(subject.convert("\e[1mHello")).to eq('<span class="term-bold">Hello</span>') + expect(subject.convert("\e[1mHello")[:html]).to eq('<span class="term-bold">Hello</span>') end it "resets bold text" do - expect(subject.convert("\e[1mHello\e[21m world")).to eq('<span class="term-bold">Hello</span> world') - expect(subject.convert("\e[1mHello\e[22m world")).to eq('<span class="term-bold">Hello</span> world') + expect(subject.convert("\e[1mHello\e[21m world")[:html]).to eq('<span class="term-bold">Hello</span> world') + expect(subject.convert("\e[1mHello\e[22m world")[:html]).to eq('<span class="term-bold">Hello</span> world') end it "prints italic text" do - expect(subject.convert("\e[3mHello")).to eq('<span class="term-italic">Hello</span>') + expect(subject.convert("\e[3mHello")[:html]).to eq('<span class="term-italic">Hello</span>') end it "resets italic text" do - expect(subject.convert("\e[3mHello\e[23m world")).to eq('<span class="term-italic">Hello</span> world') + expect(subject.convert("\e[3mHello\e[23m world")[:html]).to eq('<span class="term-italic">Hello</span> world') end it "prints underlined text" do - expect(subject.convert("\e[4mHello")).to eq('<span class="term-underline">Hello</span>') + expect(subject.convert("\e[4mHello")[:html]).to eq('<span class="term-underline">Hello</span>') end it "resets underlined text" do - expect(subject.convert("\e[4mHello\e[24m world")).to eq('<span class="term-underline">Hello</span> world') + expect(subject.convert("\e[4mHello\e[24m world")[:html]).to eq('<span class="term-underline">Hello</span> world') end it "prints concealed text" do - expect(subject.convert("\e[8mHello")).to eq('<span class="term-conceal">Hello</span>') + expect(subject.convert("\e[8mHello")[:html]).to eq('<span class="term-conceal">Hello</span>') end it "resets concealed text" do - expect(subject.convert("\e[8mHello\e[28m world")).to eq('<span class="term-conceal">Hello</span> world') + expect(subject.convert("\e[8mHello\e[28m world")[:html]).to eq('<span class="term-conceal">Hello</span> world') end it "prints crossed-out text" do - expect(subject.convert("\e[9mHello")).to eq('<span class="term-cross">Hello</span>') + expect(subject.convert("\e[9mHello")[:html]).to eq('<span class="term-cross">Hello</span>') end it "resets crossed-out text" do - expect(subject.convert("\e[9mHello\e[29m world")).to eq('<span class="term-cross">Hello</span> world') + expect(subject.convert("\e[9mHello\e[29m world")[:html]).to eq('<span class="term-cross">Hello</span> world') end it "can print 256 xterm fg colors" do - expect(subject.convert("\e[38;5;16mHello")).to eq('<span class="xterm-fg-16">Hello</span>') + expect(subject.convert("\e[38;5;16mHello")[:html]).to eq('<span class="xterm-fg-16">Hello</span>') end it "can print 256 xterm fg colors on normal magenta background" do - expect(subject.convert("\e[38;5;16;45mHello")).to eq('<span class="xterm-fg-16 term-bg-magenta">Hello</span>') + expect(subject.convert("\e[38;5;16;45mHello")[:html]).to eq('<span class="xterm-fg-16 term-bg-magenta">Hello</span>') end it "can print 256 xterm bg colors" do - expect(subject.convert("\e[48;5;240mHello")).to eq('<span class="xterm-bg-240">Hello</span>') + expect(subject.convert("\e[48;5;240mHello")[:html]).to eq('<span class="xterm-bg-240">Hello</span>') end it "can print 256 xterm bg colors on normal magenta foreground" do - expect(subject.convert("\e[48;5;16;35mHello")).to eq('<span class="term-fg-magenta xterm-bg-16">Hello</span>') + expect(subject.convert("\e[48;5;16;35mHello")[:html]).to eq('<span class="term-fg-magenta xterm-bg-16">Hello</span>') end it "prints bold colored text vividly" do - expect(subject.convert("\e[1;31mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>') + expect(subject.convert("\e[1;31mHello\e[0m")[:html]).to eq('<span class="term-fg-l-red term-bold">Hello</span>') end it "prints bold light colored text correctly" do - expect(subject.convert("\e[1;91mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>') + expect(subject.convert("\e[1;91mHello\e[0m")[:html]).to eq('<span class="term-fg-l-red term-bold">Hello</span>') + end + + it "prints <" do + expect(subject.convert("<")[:html]).to eq('<') + end + + describe "incremental update" do + shared_examples 'stateable converter' do + let(:pass1) { subject.convert(pre_text) } + let(:pass2) { subject.convert(pre_text + text, pass1[:state]) } + + it "to returns html to append" do + expect(pass2[:append]).to be_truthy + expect(pass2[:html]).to eq(html) + expect(pass1[:text] + pass2[:text]).to eq(pre_text + text) + expect(pass1[:html] + pass2[:html]).to eq(pre_html + html) + end + end + + context "with split word" do + let(:pre_text) { "\e[1mHello" } + let(:pre_html) { "<span class=\"term-bold\">Hello</span>" } + let(:text) { "\e[1mWorld" } + let(:html) { "<span class=\"term-bold\"></span><span class=\"term-bold\">World</span>" } + + it_behaves_like 'stateable converter' + end + + context "with split sequence" do + let(:pre_text) { "\e[1m" } + let(:pre_html) { "<span class=\"term-bold\"></span>" } + let(:text) { "Hello" } + let(:html) { "<span class=\"term-bold\">Hello</span>" } + + it_behaves_like 'stateable converter' + end + + context "with partial sequence" do + let(:pre_text) { "Hello\e" } + let(:pre_html) { "Hello" } + let(:text) { "[1m World" } + let(:html) { "<span class=\"term-bold\"> World</span>" } + + it_behaves_like 'stateable converter' + end + + context 'with new line' do + let(:pre_text) { "Hello\r" } + let(:pre_html) { "Hello\r" } + let(:text) { "\nWorld" } + let(:html) { "<br>World" } + + it_behaves_like 'stateable converter' + end end end diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb index 50a77308cde..9c6b4ea5086 100644 --- a/spec/lib/ci/charts_spec.rb +++ b/spec/lib/ci/charts_spec.rb @@ -4,13 +4,20 @@ describe Ci::Charts, lib: true do context "build_times" do before do - @commit = FactoryGirl.create(:ci_commit) - FactoryGirl.create(:ci_build, commit: @commit) + @pipeline = FactoryGirl.create(:ci_pipeline) + FactoryGirl.create(:ci_build, pipeline: @pipeline) end it 'should return build times in minutes' do - chart = Ci::Charts::BuildTime.new(@commit.project) + chart = Ci::Charts::BuildTime.new(@pipeline.project) expect(chart.build_times).to eq([2]) end + + it 'should handle nil build times' do + create(:ci_pipeline, duration: nil, project: @pipeline.project) + + chart = Ci::Charts::BuildTime.new(@pipeline.project) + expect(chart.build_times).to eq([2, 0]) + end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index c7ab3185378..7375539cf17 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -443,12 +443,12 @@ module Ci context 'when job variables are defined' do context 'when syntax is correct' do it 'returns job variables' do - variables = { + variables = { KEY1: 'value1', SOME_KEY_2: 'value2' } - config = YAML.dump( + config = YAML.dump( { before_script: ['pwd'], rspec: { variables: variables, @@ -619,19 +619,19 @@ module Ci context 'no dependencies' do let(:dependencies) { } - it { expect { subject }.to_not raise_error } + it { expect { subject }.not_to raise_error } end context 'dependencies to builds' do let(:dependencies) { ['build1', 'build2'] } - it { expect { subject }.to_not raise_error } + it { expect { subject }.not_to raise_error } end context 'dependencies to builds defined as symbols' do let(:dependencies) { [:build1, :build2] } - it { expect { subject }.to_not raise_error } + it { expect { subject }.not_to raise_error } end context 'undefined dependency' do diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb new file mode 100644 index 00000000000..4d8cb787dde --- /dev/null +++ b/spec/lib/container_registry/blob_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe ContainerRegistry::Blob do + let(:digest) { 'sha256:0123456789012345' } + let(:config) do + { + 'digest' => digest, + 'mediaType' => 'binary', + 'size' => 1000 + } + end + + let(:registry) { ContainerRegistry::Registry.new('http://example.com') } + let(:repository) { registry.repository('group/test') } + let(:blob) { repository.blob(config) } + + it { expect(blob).to respond_to(:repository) } + it { expect(blob).to delegate_method(:registry).to(:repository) } + it { expect(blob).to delegate_method(:client).to(:repository) } + + context '#path' do + subject { blob.path } + + it { is_expected.to eq('example.com/group/test@sha256:0123456789012345') } + end + + context '#digest' do + subject { blob.digest } + + it { is_expected.to eq(digest) } + end + + context '#type' do + subject { blob.type } + + it { is_expected.to eq('binary') } + end + + context '#revision' do + subject { blob.revision } + + it { is_expected.to eq('0123456789012345') } + end + + context '#short_revision' do + subject { blob.short_revision } + + it { is_expected.to eq('012345678') } + end + + context '#delete' do + before do + stub_request(:delete, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345'). + to_return(status: 200) + end + + subject { blob.delete } + + it { is_expected.to be_truthy } + end +end diff --git a/spec/lib/container_registry/registry_spec.rb b/spec/lib/container_registry/registry_spec.rb new file mode 100644 index 00000000000..4f3f8b24fc4 --- /dev/null +++ b/spec/lib/container_registry/registry_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe ContainerRegistry::Registry do + let(:path) { nil } + let(:registry) { described_class.new('http://example.com', path: path) } + + subject { registry } + + it { is_expected.to respond_to(:client) } + it { is_expected.to respond_to(:uri) } + it { is_expected.to respond_to(:path) } + + it { expect(subject.repository('test')).not_to be_nil } + + context '#path' do + subject { registry.path } + + context 'path from URL' do + it { is_expected.to eq('example.com') } + end + + context 'custom path' do + let(:path) { 'registry.example.com' } + + it { is_expected.to eq(path) } + end + end +end diff --git a/spec/lib/container_registry/repository_spec.rb b/spec/lib/container_registry/repository_spec.rb new file mode 100644 index 00000000000..279709521c9 --- /dev/null +++ b/spec/lib/container_registry/repository_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe ContainerRegistry::Repository do + let(:registry) { ContainerRegistry::Registry.new('http://example.com') } + let(:repository) { registry.repository('group/test') } + + it { expect(repository).to respond_to(:registry) } + it { expect(repository).to delegate_method(:client).to(:registry) } + it { expect(repository.tag('test')).not_to be_nil } + + context '#path' do + subject { repository.path } + + it { is_expected.to eq('example.com/group/test') } + end + + context 'manifest processing' do + before do + stub_request(:get, 'http://example.com/v2/group/test/tags/list'). + with(headers: { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' }). + to_return( + status: 200, + body: JSON.dump(tags: ['test']), + headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' }) + end + + context '#manifest' do + subject { repository.manifest } + + it { is_expected.not_to be_nil } + end + + context '#valid?' do + subject { repository.valid? } + + it { is_expected.to be_truthy } + end + + context '#tags' do + subject { repository.tags } + + it { is_expected.not_to be_empty } + end + end + + context '#delete_tags' do + let(:tag) { ContainerRegistry::Tag.new(repository, 'tag') } + + before { expect(repository).to receive(:tags).twice.and_return([tag]) } + + subject { repository.delete_tags } + + context 'succeeds' do + before { expect(tag).to receive(:delete).and_return(true) } + + it { is_expected.to be_truthy } + end + + context 'any fails' do + before { expect(tag).to receive(:delete).and_return(false) } + + it { is_expected.to be_falsey } + end + end +end diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb new file mode 100644 index 00000000000..858cb0bb134 --- /dev/null +++ b/spec/lib/container_registry/tag_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe ContainerRegistry::Tag do + let(:registry) { ContainerRegistry::Registry.new('http://example.com') } + let(:repository) { registry.repository('group/test') } + let(:tag) { repository.tag('tag') } + let(:headers) { { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' } } + + it { expect(tag).to respond_to(:repository) } + it { expect(tag).to delegate_method(:registry).to(:repository) } + it { expect(tag).to delegate_method(:client).to(:repository) } + + context '#path' do + subject { tag.path } + + it { is_expected.to eq('example.com/group/test:tag') } + end + + context 'manifest processing' do + before do + stub_request(:get, 'http://example.com/v2/group/test/manifests/tag'). + with(headers: headers). + to_return( + status: 200, + body: File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'), + headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' }) + end + + context '#layers' do + subject { tag.layers } + + it { expect(subject.length).to eq(1) } + end + + context '#total_size' do + subject { tag.total_size } + + it { is_expected.to eq(2319870) } + end + + context 'config processing' do + before do + stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac'). + with(headers: { 'Accept' => 'application/octet-stream' }). + to_return( + status: 200, + body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json')) + end + + context '#config' do + subject { tag.config } + + it { is_expected.not_to be_nil } + end + + context '#created_at' do + subject { tag.created_at } + + it { is_expected.not_to be_nil } + end + end + end + + context 'manifest digest' do + before do + stub_request(:head, 'http://example.com/v2/group/test/manifests/tag'). + with(headers: headers). + to_return(status: 200, headers: { 'Docker-Content-Digest' => 'sha256:digest' }) + end + + context '#digest' do + subject { tag.digest } + + it { is_expected.to eq('sha256:digest') } + end + + context '#delete' do + before do + stub_request(:delete, 'http://example.com/v2/group/test/manifests/sha256:digest'). + with(headers: headers). + to_return(status: 200) + end + + subject { tag.delete } + + it { is_expected.to be_truthy } + end + end +end diff --git a/spec/lib/disable_email_interceptor_spec.rb b/spec/lib/disable_email_interceptor_spec.rb index c2a7b20b84d..309a88151cf 100644 --- a/spec/lib/disable_email_interceptor_spec.rb +++ b/spec/lib/disable_email_interceptor_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe DisableEmailInterceptor, lib: true do before do - ActionMailer::Base.register_interceptor(DisableEmailInterceptor) + Mail.register_interceptor(DisableEmailInterceptor) end it 'should not send emails' do @@ -14,7 +14,7 @@ describe DisableEmailInterceptor, lib: true do # Removing interceptor from the list because unregister_interceptor is # implemented in later version of mail gem # See: https://github.com/mikel/mail/pull/705 - Mail.class_variable_set(:@@delivery_interceptors, []) + Mail.unregister_interceptor(DisableEmailInterceptor) end def deliver_mail diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb index 53f5d6c5c80..88a71528867 100644 --- a/spec/lib/gitlab/akismet_helper_spec.rb +++ b/spec/lib/gitlab/akismet_helper_spec.rb @@ -6,8 +6,8 @@ describe Gitlab::AkismetHelper, type: :helper do before do allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url)) - current_application_settings.akismet_enabled = true - current_application_settings.akismet_api_key = '12345' + allow_any_instance_of(ApplicationSetting).to receive(:akismet_enabled).and_return(true) + allow_any_instance_of(ApplicationSetting).to receive(:akismet_api_key).and_return('12345') end describe '#check_for_spam?' do diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index aad291c03cd..a814ad2a4e7 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -1,9 +1,47 @@ require 'spec_helper' describe Gitlab::Auth, lib: true do - let(:gl_auth) { Gitlab::Auth.new } + let(:gl_auth) { described_class } - describe :find do + describe 'find' do + it 'recognizes CI' do + token = '123' + project = create(:empty_project) + project.update_attributes(runners_token: token, builds_enabled: true) + ip = 'ip' + + expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token') + expect(gl_auth.find('gitlab-ci-token', token, project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, :ci)) + end + + it 'recognizes master passwords' do + user = create(:user, password: 'password') + ip = 'ip' + + expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username) + expect(gl_auth.find(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :gitlab_or_ldap)) + end + + it 'recognizes OAuth tokens' do + user = create(:user) + application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) + token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id) + ip = 'ip' + + expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2') + expect(gl_auth.find("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :oauth)) + end + + it 'returns double nil for invalid credentials' do + login = 'foo' + ip = 'ip' + + expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login) + expect(gl_auth.find(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new) + end + end + + describe 'find_in_gitlab_or_ldap' do let!(:user) do create(:user, username: username, @@ -14,25 +52,25 @@ describe Gitlab::Auth, lib: true do let(:password) { 'my-secret' } it "should find user by valid login/password" do - expect( gl_auth.find(username, password) ).to eql user + expect( gl_auth.find_in_gitlab_or_ldap(username, password) ).to eql user end it 'should find user by valid email/password with case-insensitive email' do - expect(gl_auth.find(user.email.upcase, password)).to eql user + expect(gl_auth.find_in_gitlab_or_ldap(user.email.upcase, password)).to eql user end it 'should find user by valid username/password with case-insensitive username' do - expect(gl_auth.find(username.upcase, password)).to eql user + expect(gl_auth.find_in_gitlab_or_ldap(username.upcase, password)).to eql user end it "should not find user with invalid password" do password = 'wrong' - expect( gl_auth.find(username, password) ).not_to eql user + expect( gl_auth.find_in_gitlab_or_ldap(username, password) ).not_to eql user end it "should not find user with invalid login" do user = 'wrong' - expect( gl_auth.find(username, password) ).not_to eql user + expect( gl_auth.find_in_gitlab_or_ldap(username, password) ).not_to eql user end context "with ldap enabled" do @@ -43,13 +81,13 @@ describe Gitlab::Auth, lib: true do it "tries to autheticate with db before ldap" do expect(Gitlab::LDAP::Authentication).not_to receive(:login) - gl_auth.find(username, password) + gl_auth.find_in_gitlab_or_ldap(username, password) end it "uses ldap as fallback to for authentication" do expect(Gitlab::LDAP::Authentication).to receive(:login) - gl_auth.find('ldap_user', 'password') + gl_auth.find_in_gitlab_or_ldap('ldap_user', 'password') end end end diff --git a/spec/lib/award_emoji_spec.rb b/spec/lib/gitlab/award_emoji_spec.rb index 88c22912950..0f3852b1729 100644 --- a/spec/lib/award_emoji_spec.rb +++ b/spec/lib/gitlab/award_emoji_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' -describe AwardEmoji do +describe Gitlab::AwardEmoji do describe '.urls' do - subject { AwardEmoji.urls } + subject { Gitlab::AwardEmoji.urls } it { is_expected.to be_an_instance_of(Array) } - it { is_expected.to_not be_empty } + it { is_expected.not_to be_empty } context 'every Hash in the Array' do it 'has the correct keys and values' do @@ -19,7 +19,7 @@ describe AwardEmoji do describe '.emoji_by_category' do it "only contains known categories" do - undefined_categories = AwardEmoji.emoji_by_category.keys - AwardEmoji::CATEGORIES.keys + undefined_categories = Gitlab::AwardEmoji.emoji_by_category.keys - Gitlab::AwardEmoji::CATEGORIES.keys expect(undefined_categories).to be_empty end end diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb deleted file mode 100644 index cd26dca0998..00000000000 --- a/spec/lib/gitlab/backend/grack_auth_spec.rb +++ /dev/null @@ -1,209 +0,0 @@ -require "spec_helper" - -describe Grack::Auth, lib: true do - let(:user) { create(:user) } - let(:project) { create(:project) } - - let(:app) { lambda { |env| [200, {}, "Success!"] } } - let!(:auth) { Grack::Auth.new(app) } - let(:env) do - { - 'rack.input' => '', - 'REQUEST_METHOD' => 'GET', - 'QUERY_STRING' => 'service=git-upload-pack' - } - end - let(:status) { auth.call(env).first } - - describe "#call" do - context "when the project doesn't exist" do - before do - env["PATH_INFO"] = "doesnt/exist.git" - end - - context "when no authentication is provided" do - it "responds with status 401" do - expect(status).to eq(401) - end - end - - context "when username and password are provided" do - context "when authentication fails" do - before do - env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, "nope") - end - - it "responds with status 401" do - expect(status).to eq(401) - end - end - - context "when authentication succeeds" do - before do - env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) - end - - it "responds with status 404" do - expect(status).to eq(404) - end - end - end - end - - context "when the Wiki for a project exists" do - before do - @wiki = ProjectWiki.new(project) - env["PATH_INFO"] = "#{@wiki.repository.path_with_namespace}.git/info/refs" - project.update_attribute(:visibility_level, Project::PUBLIC) - end - - it "responds with the right project" do - response = auth.call(env) - json_body = ActiveSupport::JSON.decode(response[2][0]) - - expect(response.first).to eq(200) - expect(json_body['RepoPath']).to include(@wiki.repository.path_with_namespace) - end - end - - context "when the project exists" do - before do - env["PATH_INFO"] = project.path_with_namespace + ".git" - end - - context "when the project is public" do - before do - project.update_attribute(:visibility_level, Project::PUBLIC) - end - - it "responds with status 200" do - expect(status).to eq(200) - end - end - - context "when the project is private" do - before do - project.update_attribute(:visibility_level, Project::PRIVATE) - end - - context "when no authentication is provided" do - it "responds with status 401" do - expect(status).to eq(401) - end - end - - context "when username and password are provided" do - context "when authentication fails" do - before do - env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, "nope") - end - - it "responds with status 401" do - expect(status).to eq(401) - end - - context "when the user is IP banned" do - before do - expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true) - allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4') - end - - it "responds with status 401" do - expect(status).to eq(401) - end - end - end - - context "when authentication succeeds" do - before do - env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) - end - - context "when the user has access to the project" do - before do - project.team << [user, :master] - end - - context "when the user is blocked" do - before do - user.block - project.team << [user, :master] - end - - it "responds with status 404" do - expect(status).to eq(404) - end - end - - context "when the user isn't blocked" do - before do - expect(Rack::Attack::Allow2Ban).to receive(:reset) - end - - it "responds with status 200" do - expect(status).to eq(200) - end - end - - context "when blank password attempts follow a valid login" do - let(:options) { Gitlab.config.rack_attack.git_basic_auth } - let(:maxretry) { options[:maxretry] - 1 } - let(:ip) { '1.2.3.4' } - - before do - allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip) - Rack::Attack::Allow2Ban.reset(ip, options) - end - - after do - Rack::Attack::Allow2Ban.reset(ip, options) - end - - def attempt_login(include_password) - password = include_password ? user.password : "" - env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, password) - Grack::Auth.new(app) - auth.call(env).first - end - - it "repeated attempts followed by successful attempt" do - maxretry.times.each do - expect(attempt_login(false)).to eq(401) - end - - expect(attempt_login(true)).to eq(200) - expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey - - maxretry.times.each do - expect(attempt_login(false)).to eq(401) - end - end - end - end - - context "when the user doesn't have access to the project" do - it "responds with status 404" do - expect(status).to eq(404) - end - end - end - end - - context "when a gitlab ci token is provided" do - let(:token) { "123" } - let(:project) { FactoryGirl.create :empty_project } - - before do - project.update_attributes(runners_token: token, builds_enabled: true) - - env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials("gitlab-ci-token", token) - end - - it "responds with status 200" do - expect(status).to eq(200) - end - end - end - end - end -end diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb index b6f7a2e7ec4..2034445a197 100644 --- a/spec/lib/gitlab/badge/build_spec.rb +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -42,9 +42,7 @@ describe Gitlab::Badge::Build do end context 'build exists' do - let(:ci_commit) { create(:ci_commit, project: project, sha: sha, ref: branch) } - let!(:build) { create(:ci_build, commit: ci_commit) } - + let!(:build) { create_build(project, sha, branch) } context 'build success' do before { build.success! } @@ -96,6 +94,28 @@ describe Gitlab::Badge::Build do end end + context 'when outdated pipeline for given ref exists' do + before do + build = create_build(project, sha, branch) + build.success! + + old_build = create_build(project, '11eeffdd', branch) + old_build.drop! + end + + it 'does not take outdated pipeline into account' do + expect(badge.to_s).to eq 'build-success' + end + end + + def create_build(project, sha, branch) + pipeline = create(:ci_pipeline, project: project, + sha: sha, + ref: branch) + + create(:ci_build, pipeline: pipeline) + end + def status_node(data, status) xml = Nokogiri::XML.parse(data) xml.at(%Q{text:contains("#{status}")}) diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb index af839f42421..760d66a1488 100644 --- a/spec/lib/gitlab/bitbucket_import/client_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/client_spec.rb @@ -1,12 +1,14 @@ require 'spec_helper' describe Gitlab::BitbucketImport::Client, lib: true do + include ImportSpecHelper + let(:token) { '123456' } let(:secret) { 'secret' } let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) } before do - Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket") + stub_omniauth_provider('bitbucket') end it 'all OAuth client options are symbols' do @@ -59,7 +61,7 @@ describe Gitlab::BitbucketImport::Client, lib: true do bitbucket_access_token_secret: "test" } }) project.import_url = "ssh://git@bitbucket.org/test/test.git" - expect { described_class.from_project(project) }.to_not raise_error + expect { described_class.from_project(project) }.not_to raise_error end end end diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index 1a833f255a5..aa00f32becb 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -1,8 +1,10 @@ require 'spec_helper' describe Gitlab::BitbucketImport::Importer, lib: true do + include ImportSpecHelper + before do - Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket") + stub_omniauth_provider('bitbucket') end let(:statuses) do diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb index acca0b08bab..711a3e1c7d4 100644 --- a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb +++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb @@ -10,8 +10,8 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do 'path/dir_1/subdir/subfile' => { size: 10 }, 'path/second_dir' => {}, 'path/second_dir/dir_3/file_2' => { size: 10 }, - 'path/second_dir/dir_3/file_3'=> { size: 10 }, - 'another_directory/'=> {}, + 'path/second_dir/dir_3/file_3' => { size: 10 }, + 'another_directory/' => {}, 'another_file' => {}, '/file/with/absolute_path' => {} } end @@ -122,7 +122,7 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do describe 'empty path', path: '' do subject { |example| path(example) } - it { is_expected.to_not have_parent } + it { is_expected.not_to have_parent } describe '#children' do subject { |example| path(example).children } diff --git a/spec/lib/gitlab/ci/config/loader_spec.rb b/spec/lib/gitlab/ci/config/loader_spec.rb new file mode 100644 index 00000000000..2d44b1f60f1 --- /dev/null +++ b/spec/lib/gitlab/ci/config/loader_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Loader do + let(:loader) { described_class.new(yml) } + + context 'when yaml syntax is correct' do + let(:yml) { 'image: ruby:2.2' } + + describe '#valid?' do + it 'returns true' do + expect(loader.valid?).to be true + end + end + + describe '#load!' do + it 'returns a valid hash' do + expect(loader.load!).to eq(image: 'ruby:2.2') + end + end + end + + context 'when yaml syntax is incorrect' do + let(:yml) { '// incorrect' } + + describe '#valid?' do + it 'returns false' do + expect(loader.valid?).to be false + end + end + + describe '#load!' do + it 'raises error' do + expect { loader.load! }.to raise_error( + Gitlab::Ci::Config::Loader::FormatError, + 'Invalid configuration format' + ) + end + end + end + + context 'when yaml config is empty' do + let(:yml) { '' } + + describe '#valid?' do + it 'returns false' do + expect(loader.valid?).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb new file mode 100644 index 00000000000..4d46abe520f --- /dev/null +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config do + let(:config) do + described_class.new(yml) + end + + context 'when config is valid' do + let(:yml) do + <<-EOS + image: ruby:2.2 + + rspec: + script: + - gem install rspec + - rspec + EOS + end + + describe '#to_hash' do + it 'returns hash created from string' do + hash = { + image: 'ruby:2.2', + rspec: { + script: ['gem install rspec', + 'rspec'] + } + } + + expect(config.to_hash).to eq hash + end + end + + context 'when config is invalid' do + let(:yml) { '// invalid' } + + describe '.new' do + it 'raises error' do + expect { config }.to raise_error( + Gitlab::Ci::Config::Loader::FormatError, + /Invalid configuration format/ + ) + end + end + end + end +end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb new file mode 100644 index 00000000000..83ddabe6b0b --- /dev/null +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' + +describe Gitlab::Database::MigrationHelpers, lib: true do + let(:model) do + ActiveRecord::Migration.new.extend( + Gitlab::Database::MigrationHelpers + ) + end + + before { allow(model).to receive(:puts) } + + describe '#add_concurrent_index' do + context 'outside a transaction' do + before do + expect(model).to receive(:transaction_open?).and_return(false) + end + + context 'using PostgreSQL' do + before { expect(Gitlab::Database).to receive(:postgresql?).and_return(true) } + + it 'creates the index concurrently' do + expect(model).to receive(:add_index). + with(:users, :foo, algorithm: :concurrently) + + model.add_concurrent_index(:users, :foo) + end + + it 'creates unique index concurrently' do + expect(model).to receive(:add_index). + with(:users, :foo, { algorithm: :concurrently, unique: true }) + + model.add_concurrent_index(:users, :foo, unique: true) + end + end + + context 'using MySQL' do + it 'creates a regular index' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(false) + + expect(model).to receive(:add_index). + with(:users, :foo, {}) + + model.add_concurrent_index(:users, :foo) + end + end + end + + context 'inside a transaction' do + it 'raises RuntimeError' do + expect(model).to receive(:transaction_open?).and_return(true) + + expect { model.add_concurrent_index(:users, :foo) }. + to raise_error(RuntimeError) + end + end + end + + describe '#update_column_in_batches' do + before do + create_list(:empty_project, 5) + end + + it 'updates all the rows in a table' do + model.update_column_in_batches(:projects, :import_error, 'foo') + + expect(Project.where(import_error: 'foo').count).to eq(5) + end + + it 'updates boolean values correctly' do + model.update_column_in_batches(:projects, :archived, true) + + expect(Project.where(archived: true).count).to eq(5) + end + end + + describe '#add_column_with_default' do + context 'outside of a transaction' do + before do + expect(model).to receive(:transaction_open?).and_return(false) + + expect(model).to receive(:transaction).twice.and_yield + + expect(model).to receive(:add_column). + with(:projects, :foo, :integer, default: nil) + + expect(model).to receive(:change_column_default). + with(:projects, :foo, 10) + end + + it 'adds the column while allowing NULL values' do + expect(model).to receive(:update_column_in_batches). + with(:projects, :foo, 10) + + expect(model).not_to receive(:change_column_null) + + model.add_column_with_default(:projects, :foo, :integer, + default: 10, + allow_null: true) + end + + it 'adds the column while not allowing NULL values' do + expect(model).to receive(:update_column_in_batches). + with(:projects, :foo, 10) + + expect(model).to receive(:change_column_null). + with(:projects, :foo, false) + + model.add_column_with_default(:projects, :foo, :integer, default: 10) + end + + it 'removes the added column whenever updating the rows fails' do + expect(model).to receive(:update_column_in_batches). + with(:projects, :foo, 10). + and_raise(RuntimeError) + + expect(model).to receive(:remove_column). + with(:projects, :foo) + + expect do + model.add_column_with_default(:projects, :foo, :integer, default: 10) + end.to raise_error(RuntimeError) + end + end + + context 'inside a transaction' do + it 'raises RuntimeError' do + expect(model).to receive(:transaction_open?).and_return(true) + + expect do + model.add_column_with_default(:projects, :foo, :integer, default: 10) + end.to raise_error(RuntimeError) + end + end + end +end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index d0a447753b7..3031559c613 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -39,6 +39,22 @@ describe Gitlab::Database, lib: true do end end + describe '.nulls_last_order' do + context 'when using PostgreSQL' do + before { expect(described_class).to receive(:postgresql?).and_return(true) } + + it { expect(described_class.nulls_last_order('column', 'ASC')).to eq 'column ASC NULLS LAST'} + it { expect(described_class.nulls_last_order('column', 'DESC')).to eq 'column DESC NULLS LAST'} + end + + context 'when using MySQL' do + before { expect(described_class).to receive(:postgresql?).and_return(false) } + + it { expect(described_class.nulls_last_order('column', 'ASC')).to eq 'column IS NULL, column ASC'} + it { expect(described_class.nulls_last_order('column', 'DESC')).to eq 'column DESC'} + end + end + describe '#true_value' do it 'returns correct value for PostgreSQL' do expect(described_class).to receive(:postgresql?).and_return(true) diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb index b2d7a799810..c19f33e2224 100644 --- a/spec/lib/gitlab/email/message/repository_push_spec.rb +++ b/spec/lib/gitlab/email/message/repository_push_spec.rb @@ -8,7 +8,7 @@ describe Gitlab::Email::Message::RepositoryPush do let!(:author) { create(:author, name: 'Author') } let(:message) do - described_class.new(Notify, project.id, 'recipient@example.com', opts) + described_class.new(Notify, project.id, opts) end context 'new commits have been pushed to repository' do @@ -57,7 +57,7 @@ describe Gitlab::Email::Message::RepositoryPush do describe '#diffs' do subject { message.diffs } - it { is_expected.to all(be_an_instance_of Gitlab::Git::Diff) } + it { is_expected.to all(be_an_instance_of Gitlab::Diff::File) } end describe '#diffs_count' do diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index 0a7ca3ec848..0af249d8690 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -33,8 +33,8 @@ describe Gitlab::Gfm::ReferenceRewriter do end it { is_expected.to include issue_first.to_reference(new_project) } - it { is_expected.to_not include issue_second.to_reference(new_project) } - it { is_expected.to_not include merge_request.to_reference(new_project) } + it { is_expected.not_to include issue_second.to_reference(new_project) } + it { is_expected.not_to include merge_request.to_reference(new_project) } end context 'description ambigous elements' do diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb index eda956e6f0a..6eca33f9fee 100644 --- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb @@ -32,13 +32,13 @@ describe Gitlab::Gfm::UploadsRewriter do let(:new_paths) { new_files.map(&:path) } it 'rewrites content' do - expect(new_text).to_not eq text + expect(new_text).not_to eq text expect(new_text.length).to eq text.length end it 'copies files' do expect(new_files).to all(exist) - expect(old_paths).to_not match_array new_paths + expect(old_paths).not_to match_array new_paths expect(old_paths).to all(include(old_project.path_with_namespace)) expect(new_paths).to all(include(new_project.path_with_namespace)) end @@ -48,8 +48,8 @@ describe Gitlab::Gfm::UploadsRewriter do end it 'generates a new secret for each file' do - expect(new_paths).to_not include image_uploader.secret - expect(new_paths).to_not include zip_uploader.secret + expect(new_paths).not_to include image_uploader.secret + expect(new_paths).not_to include zip_uploader.secret end end diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb new file mode 100644 index 00000000000..3cb634ba010 --- /dev/null +++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::BranchFormatter, lib: true do + let(:project) { create(:project) } + let(:repo) { double } + let(:raw) do + { + ref: 'feature', + repo: repo, + sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + } + end + + describe '#exists?' do + it 'returns true when branch exists' do + branch = described_class.new(project, double(raw)) + + expect(branch.exists?).to eq true + end + + it 'returns false when branch does not exist' do + branch = described_class.new(project, double(raw.merge(ref: 'removed-branch'))) + + expect(branch.exists?).to eq false + end + end + + describe '#name' do + it 'returns raw ref when branch exists' do + branch = described_class.new(project, double(raw)) + + expect(branch.name).to eq 'feature' + end + + it 'returns formatted ref when branch does not exist' do + branch = described_class.new(project, double(raw.merge(ref: 'removed-branch'))) + + expect(branch.name).to eq 'removed-branch-2e5d3239' + end + end + + describe '#repo' do + it 'returns raw repo' do + branch = described_class.new(project, double(raw)) + + expect(branch.repo).to eq repo + end + end + + describe '#sha' do + it 'returns raw sha' do + branch = described_class.new(project, double(raw)) + + expect(branch.sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + end + end + + describe '#valid?' do + it 'returns true when repository exists' do + branch = described_class.new(project, double(raw)) + + expect(branch.valid?).to eq true + end + + it 'returns false when repository does not exist' do + branch = described_class.new(project, double(raw.merge(repo: nil))) + + expect(branch.valid?).to eq false + end + end +end diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb index 55e86d4ceac..9ae02a6c45f 100644 --- a/spec/lib/gitlab/github_import/comment_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb @@ -29,6 +29,7 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do commit_id: nil, line_code: nil, author_id: project.creator_id, + type: nil, created_at: created_at, updated_at: updated_at } @@ -56,6 +57,7 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e', line_code: 'ce1be0ff4065a6e9415095c95f25f47a633cef2b_4_3', author_id: project.creator_id, + type: 'LegacyDiffNote', created_at: created_at, updated_at: updated_at } diff --git a/spec/lib/gitlab/github_import/hook_formatter_spec.rb b/spec/lib/gitlab/github_import/hook_formatter_spec.rb new file mode 100644 index 00000000000..110ba428258 --- /dev/null +++ b/spec/lib/gitlab/github_import/hook_formatter_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::HookFormatter, lib: true do + describe '#id' do + it 'returns raw id' do + raw = double(id: 100000) + formatter = described_class.new(raw) + expect(formatter.id).to eq 100000 + end + end + + describe '#name' do + it 'returns raw id' do + raw = double(name: 'web') + formatter = described_class.new(raw) + expect(formatter.name).to eq 'web' + end + end + + describe '#config' do + it 'returns raw config.attrs' do + raw = double(config: double(attrs: { url: 'http://something.com/webhook' })) + formatter = described_class.new(raw) + expect(formatter.config).to eq({ url: 'http://something.com/webhook' }) + end + end + + describe '#valid?' do + it 'returns true when events contains the wildcard event' do + raw = double(events: ['*', 'commit_comment'], active: true) + formatter = described_class.new(raw) + expect(formatter.valid?).to eq true + end + + it 'returns true when events contains the create event' do + raw = double(events: ['create', 'commit_comment'], active: true) + formatter = described_class.new(raw) + expect(formatter.valid?).to eq true + end + + it 'returns true when events contains delete event' do + raw = double(events: ['delete', 'commit_comment'], active: true) + formatter = described_class.new(raw) + expect(formatter.valid?).to eq true + end + + it 'returns true when events contains pull_request event' do + raw = double(events: ['pull_request', 'commit_comment'], active: true) + formatter = described_class.new(raw) + expect(formatter.valid?).to eq true + end + + it 'returns false when events does not contains branch related events' do + raw = double(events: ['member', 'commit_comment'], active: true) + formatter = described_class.new(raw) + expect(formatter.valid?).to eq false + end + + it 'returns false when hook is not active' do + raw = double(events: ['pull_request', 'commit_comment'], active: false) + formatter = described_class.new(raw) + expect(formatter.valid?).to eq false + end + end +end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index e59c0ca110e..120f59e6e71 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -4,9 +4,9 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:project) { create(:project) } let(:repository) { double(id: 1, fork: false) } let(:source_repo) { repository } - let(:source_branch) { double(ref: 'feature', repo: source_repo) } + let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } let(:target_repo) { repository } - let(:target_branch) { double(ref: 'master', repo: target_repo) } + let(:target_branch) { double(ref: 'master', repo: target_repo, sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7') } let(:octocat) { double(id: 123456, login: 'octocat') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -41,8 +41,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', + head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', target_project: project, target_branch: 'master', + base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', state: 'opened', milestone: nil, author_id: project.creator_id, @@ -66,8 +68,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', + head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', target_project: project, target_branch: 'master', + base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', state: 'closed', milestone: nil, author_id: project.creator_id, @@ -91,8 +95,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', + head_source_sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', target_project: project, target_branch: 'master', + base_target_sha: '8ffb3c15a5475e59ae909384297fede4badcb4c7', state: 'merged', milestone: nil, author_id: project.creator_id, @@ -137,11 +143,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:milestone) { double(number: 45) } let(:raw_data) { double(base_data.merge(milestone: milestone)) } - it 'returns nil when milestone does not exists' do + it 'returns nil when milestone does not exist' do expect(pull_request.attributes.fetch(:milestone)).to be_nil end - it 'returns milestone when is exists' do + it 'returns milestone when it exists' do milestone = create(:milestone, project: project, iid: 45) expect(pull_request.attributes.fetch(:milestone)).to eq milestone @@ -158,36 +164,16 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end describe '#valid?' do - let(:invalid_branch) { double(ref: 'invalid-branch').as_null_object } - - context 'when source, and target repositories are the same' do - context 'and source and target branches exists' do - let(:raw_data) { double(base_data.merge(head: source_branch, base: target_branch)) } - - it 'returns true' do - expect(pull_request.valid?).to eq true - end - end - - context 'and source branch doesn not exists' do - let(:raw_data) { double(base_data.merge(head: invalid_branch, base: target_branch)) } - - it 'returns false' do - expect(pull_request.valid?).to eq false - end - end - - context 'and target branch doesn not exists' do - let(:raw_data) { double(base_data.merge(head: source_branch, base: invalid_branch)) } + context 'when source, and target repos are not a fork' do + let(:raw_data) { double(base_data) } - it 'returns false' do - expect(pull_request.valid?).to eq false - end + it 'returns true' do + expect(pull_request.valid?).to eq true end end context 'when source repo is a fork' do - let(:source_repo) { double(id: 2, fork: true) } + let(:source_repo) { double(id: 2) } let(:raw_data) { double(base_data) } it 'returns false' do @@ -196,7 +182,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end context 'when target repo is a fork' do - let(:target_repo) { double(id: 2, fork: true) } + let(:target_repo) { double(id: 2) } let(:raw_data) { double(base_data) } it 'returns false' do diff --git a/spec/lib/gitlab/gitignore_spec.rb b/spec/lib/gitlab/gitignore_spec.rb new file mode 100644 index 00000000000..72baa516cc4 --- /dev/null +++ b/spec/lib/gitlab/gitignore_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::Gitignore do + subject { Gitlab::Gitignore } + + describe '.all' do + it 'strips the gitignore suffix' do + expect(subject.all.first.name).not_to end_with('.gitignore') + end + + it 'combines the globals and rest' do + all = subject.all.map(&:name) + + expect(all).to include('Vim') + expect(all).to include('Ruby') + end + end + + describe '.find' do + it 'returns nil if the file does not exist' do + expect(subject.find('mepmep-yadida')).to be nil + end + + it 'returns the Gitignore object of a valid file' do + ruby = subject.find('Ruby') + + expect(ruby).to be_a Gitlab::Gitignore + expect(ruby.name).to eq('Ruby') + end + end + + describe '#content' do + it 'loads the full file' do + gitignore = subject.new(Rails.root.join('vendor/gitignore/Ruby.gitignore')) + + expect(gitignore.name).to eq 'Ruby' + expect(gitignore.content).to start_with('*.gem') + end + end +end diff --git a/spec/lib/gitlab/gitlab_import/client_spec.rb b/spec/lib/gitlab/gitlab_import/client_spec.rb index e6831e7c383..cd8e805466a 100644 --- a/spec/lib/gitlab/gitlab_import/client_spec.rb +++ b/spec/lib/gitlab/gitlab_import/client_spec.rb @@ -1,11 +1,13 @@ require 'spec_helper' describe Gitlab::GitlabImport::Client, lib: true do + include ImportSpecHelper + let(:token) { '123456' } let(:client) { Gitlab::GitlabImport::Client.new(token) } before do - Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "gitlab") + stub_omniauth_provider('gitlab') end it 'all OAuth2 client options are symbols' do diff --git a/spec/lib/gitlab/import_export/import_export_reader_spec.rb b/spec/lib/gitlab/import_export/import_export_reader_spec.rb index c4d03fecd54..19f9bdc8d3f 100644 --- a/spec/lib/gitlab/import_export/import_export_reader_spec.rb +++ b/spec/lib/gitlab/import_export/import_export_reader_spec.rb @@ -16,7 +16,11 @@ describe Gitlab::ImportExport::ImportExportReader, lib: true do } end + before do + allow_any_instance_of(Gitlab::ImportExport).to receive(:config_file).and_return(test_config) + end + it 'generates hash from project tree config' do - expect(described_class.new(config: test_config, shared: shared).project_tree).to match(project_tree_hash) + expect(described_class.new(shared: shared).project_tree).to match(project_tree_hash) end end diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 1c55d0e33c1..6e6adfd60eb 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -53,6 +53,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['snippets']).not_to be_empty end + it 'has snippet notes' do + expect(saved_project_json['snippets'].first['notes']).not_to be_empty + end + it 'has releases' do expect(saved_project_json['releases']).not_to be_empty end @@ -85,16 +89,20 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['merge_requests'].first['notes'].first['author']).not_to be_empty end - it 'has commit statuses' do - expect(saved_project_json['ci_commits'].first['statuses']).not_to be_empty + it 'has pipeline statuses' do + expect(saved_project_json['pipelines'].first['statuses']).not_to be_empty + end + + it 'has pipeline builds' do + expect(saved_project_json['pipelines'].first['statuses'].first['type']).to eq('Ci::Build') end - it 'has CI builds' do - expect(saved_project_json['ci_commits'].first['statuses'].first['type']).to eq('Ci::Build') + it 'has pipeline commits' do + expect(saved_project_json['pipelines']).not_to be_empty end - it 'has ci commits' do - expect(saved_project_json['ci_commits']).not_to be_empty + it 'has ci pipeline notes' do + expect(saved_project_json['pipelines'].first['notes']).not_to be_empty end end end @@ -115,11 +123,23 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do releases: [release] ) - ci_commit = create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) - create(:ci_build, commit: ci_commit) + commit_status = create(:commit_status, project: project) + + ci_pipeline = create(:ci_pipeline, + project: project, + sha: merge_request.last_commit.id, + ref: merge_request.source_branch, + statuses: [commit_status]) + + create(:ci_build, pipeline: ci_pipeline, project: project) create(:milestone, project: project) - create(:note, noteable: issue) - create(:note, noteable: merge_request) + create(:note, noteable: issue, project: project) + create(:note, noteable: merge_request, project: project) + create(:note, noteable: snippet, project: project) + create(:note_on_commit, + author: user, + project: project, + commit_id: ci_pipeline.sha) project end diff --git a/spec/lib/gitlab/import_export/repo_bundler_spec.rb b/spec/lib/gitlab/import_export/repo_bundler_spec.rb index fa926bab823..590a9a7e1a5 100644 --- a/spec/lib/gitlab/import_export/repo_bundler_spec.rb +++ b/spec/lib/gitlab/import_export/repo_bundler_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::ImportExport::RepoBundler, services: true do +describe Gitlab::ImportExport::RepoSaver, services: true do describe 'bundle a project Git repo' do let(:user) { create(:user) } @@ -19,7 +19,7 @@ describe Gitlab::ImportExport::RepoBundler, services: true do end it 'bundles the repo successfully' do - expect(bundler.bundle).to be true + expect(bundler.save).to be true end end end diff --git a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb index 60a0145c1a0..b9ffc8694a5 100644 --- a/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb +++ b/spec/lib/gitlab/import_export/wiki_repo_bundler_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::ImportExport::WikiRepoBundler, services: true do +describe Gitlab::ImportExport::WikiRepoSaver, services: true do describe 'bundle a wiki Git repo' do let(:user) { create(:user) } @@ -22,7 +22,7 @@ describe Gitlab::ImportExport::WikiRepoBundler, services: true do end it 'bundles the repo successfully' do - expect(wiki_bundler.bundle).to be true + expect(wiki_bundler.save).to be true end end end diff --git a/spec/lib/gitlab/import_url_spec.rb b/spec/lib/gitlab/import_url_spec.rb deleted file mode 100644 index f758cb8693c..00000000000 --- a/spec/lib/gitlab/import_url_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ImportUrl do - - let(:credentials) { { user: 'blah', password: 'password' } } - let(:import_url) do - Gitlab::ImportUrl.new("https://github.com/me/project.git", credentials: credentials) - end - - describe :full_url do - it { expect(import_url.full_url).to eq("https://blah:password@github.com/me/project.git") } - end - - describe :sanitized_url do - it { expect(import_url.sanitized_url).to eq("https://github.com/me/project.git") } - end - - describe :credentials do - it { expect(import_url.credentials).to eq(credentials) } - end -end diff --git a/spec/lib/gitlab/lazy_spec.rb b/spec/lib/gitlab/lazy_spec.rb new file mode 100644 index 00000000000..b5ca89dd242 --- /dev/null +++ b/spec/lib/gitlab/lazy_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Lazy, lib: true do + let(:dummy) { double(:dummy) } + + context 'when not calling any methods' do + it 'does not call the supplied block' do + expect(dummy).not_to receive(:foo) + + described_class.new { dummy.foo } + end + end + + context 'when calling a method on the object' do + it 'lazy loads the value returned by the block' do + expect(dummy).to receive(:foo).and_return('foo') + + lazy = described_class.new { dummy.foo } + + expect(lazy.to_s).to eq('foo') + end + end + + describe '#respond_to?' do + it 'returns true for a method defined on the wrapped object' do + lazy = described_class.new { 'foo' } + + expect(lazy).to respond_to(:downcase) + end + + it 'returns false for a method not defined on the wrapped object' do + lazy = described_class.new { 'foo' } + + expect(lazy).not_to respond_to(:quack) + end + end +end diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb index 5852b31ab3a..88814bc474d 100644 --- a/spec/lib/gitlab/lfs/lfs_router_spec.rb +++ b/spec/lib/gitlab/lfs/lfs_router_spec.rb @@ -26,8 +26,8 @@ describe Gitlab::Lfs::Router, lib: true do let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" } let(:sample_size) { 499013 } - let(:respond_with_deprecated) {[ 501, { "Content-Type"=>"application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} - let(:respond_with_disabled) {[ 501, { "Content-Type"=>"application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} + let(:respond_with_deprecated) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} + let(:respond_with_disabled) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} describe 'when lfs is disabled' do before do @@ -368,7 +368,7 @@ describe Gitlab::Lfs::Router, lib: true do expect(response['objects']).to be_kind_of(Array) expect(response['objects'].first['oid']).to eq(sample_oid) expect(response['objects'].first['size']).to eq(sample_size) - expect(lfs_object.projects.pluck(:id)).to_not include(project.id) + expect(lfs_object.projects.pluck(:id)).not_to include(project.id) expect(lfs_object.projects.pluck(:id)).to include(public_project.id) expect(response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}") expect(response['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth) @@ -430,7 +430,7 @@ describe Gitlab::Lfs::Router, lib: true do expect(response_body['objects'].last['oid']).to eq(sample_oid) expect(response_body['objects'].last['size']).to eq(sample_size) - expect(response_body['objects'].last).to_not have_key('actions') + expect(response_body['objects'].last).not_to have_key('actions') end end end diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index 5c885a7a982..220e86924a2 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -56,9 +56,6 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) - expect(transaction).to receive(:increment). - with(:method_duration, a_kind_of(Numeric)) - expect(transaction).to receive(:add_metric). with(described_class::SERIES, an_instance_of(Hash), method: 'Dummy.foo') @@ -70,7 +67,7 @@ describe Gitlab::Metrics::Instrumentation do allow(Gitlab::Metrics).to receive(:method_call_threshold). and_return(100) - expect(transaction).to_not receive(:add_metric) + expect(transaction).not_to receive(:add_metric) @dummy.foo end @@ -139,9 +136,6 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) - expect(transaction).to receive(:increment). - with(:method_duration, a_kind_of(Numeric)) - expect(transaction).to receive(:add_metric). with(described_class::SERIES, an_instance_of(Hash), method: 'Dummy#bar') @@ -153,7 +147,7 @@ describe Gitlab::Metrics::Instrumentation do allow(Gitlab::Metrics).to receive(:method_call_threshold). and_return(100) - expect(transaction).to_not receive(:add_metric) + expect(transaction).not_to receive(:add_metric) @dummy.new.bar end @@ -226,7 +220,7 @@ describe Gitlab::Metrics::Instrumentation do described_class.instrument_methods(@dummy) - expect(@dummy).to_not respond_to(:_original_kittens) + expect(@dummy).not_to respond_to(:_original_kittens) end it 'can take a block to determine if a method should be instrumented' do @@ -234,7 +228,7 @@ describe Gitlab::Metrics::Instrumentation do false end - expect(@dummy).to_not respond_to(:_original_foo) + expect(@dummy).not_to respond_to(:_original_foo) end end diff --git a/spec/lib/gitlab/metrics/sampler_spec.rb b/spec/lib/gitlab/metrics/sampler_spec.rb index 38da77adc9f..59db127674a 100644 --- a/spec/lib/gitlab/metrics/sampler_spec.rb +++ b/spec/lib/gitlab/metrics/sampler_spec.rb @@ -130,7 +130,7 @@ describe Gitlab::Metrics::Sampler do 100.times do interval = sampler.sleep_interval - expect(interval).to_not eq(last) + expect(interval).not_to eq(last) last = interval end diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index e3293a01207..49699ffe28f 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::Metrics::Subscribers::ActiveRecord do describe 'without a current transaction' do it 'simply returns' do expect_any_instance_of(Gitlab::Metrics::Transaction). - to_not receive(:increment) + not_to receive(:increment) subscriber.sql(event) end diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb index e01b0b4bd21..d824dc54438 100644 --- a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_read' do it 'increments the cache_read duration' do expect(subscriber).to receive(:increment). - with(:cache_read_duration, event.duration) + with(:cache_read, event.duration) subscriber.cache_read(event) end @@ -18,7 +18,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_write' do it 'increments the cache_write duration' do expect(subscriber).to receive(:increment). - with(:cache_write_duration, event.duration) + with(:cache_write, event.duration) subscriber.cache_write(event) end @@ -27,7 +27,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_delete' do it 'increments the cache_delete duration' do expect(subscriber).to receive(:increment). - with(:cache_delete_duration, event.duration) + with(:cache_delete, event.duration) subscriber.cache_delete(event) end @@ -36,7 +36,7 @@ describe Gitlab::Metrics::Subscribers::RailsCache do describe '#cache_exist?' do it 'increments the cache_exists duration' do expect(subscriber).to receive(:increment). - with(:cache_exists_duration, event.duration) + with(:cache_exists, event.duration) subscriber.cache_exist?(event) end @@ -62,9 +62,15 @@ describe Gitlab::Metrics::Subscribers::RailsCache do with(:cache_duration, event.duration) expect(transaction).to receive(:increment). + with(:cache_count, 1) + + expect(transaction).to receive(:increment). with(:cache_delete_duration, event.duration) - subscriber.increment(:cache_delete_duration, event.duration) + expect(transaction).to receive(:increment). + with(:cache_delete_count, 1) + + subscriber.increment(:cache_delete, event.duration) end end end diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb new file mode 100644 index 00000000000..fd6f684db0c --- /dev/null +++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Gitlab::Middleware::RailsQueueDuration do + let(:app) { double(:app) } + let(:middleware) { described_class.new(app) } + let(:env) { {} } + let(:transaction) { double(:transaction) } + + before { expect(app).to receive(:call).with(env).and_return('yay') } + + describe '#call' do + it 'calls the app when metrics are disabled' do + expect(Gitlab::Metrics).to receive(:current_transaction).and_return(nil) + expect(middleware.call(env)).to eq('yay') + end + + context 'when metrics are enabled' do + before { allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction) } + + it 'calls the app when metrics are enabled but no timing header is found' do + expect(middleware.call(env)).to eq('yay') + end + + it 'sets proxy_flight_time and calls the app when the header is present' do + env['HTTP_GITLAB_WORHORSE_PROXY_START'] = '123' + expect(transaction).to receive(:set).with(:rails_queue_duration, an_instance_of(Float)) + expect(middleware.call(env)).to eq('yay') + end + end + end +end diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb index f093d0a0d8b..e848d88182f 100644 --- a/spec/lib/gitlab/note_data_builder_spec.rb +++ b/spec/lib/gitlab/note_data_builder_spec.rb @@ -9,7 +9,8 @@ describe 'Gitlab::NoteDataBuilder', lib: true do before(:each) do expect(data).to have_key(:object_attributes) expect(data[:object_attributes]).to have_key(:url) - expect(data[:object_attributes][:url]).to eq(Gitlab::UrlBuilder.build(note)) + expect(data[:object_attributes][:url]) + .to eq(Gitlab::UrlBuilder.build(note)) expect(data[:object_kind]).to eq('note') expect(data[:user]).to eq(user.hook_attrs) end @@ -37,13 +38,21 @@ describe 'Gitlab::NoteDataBuilder', lib: true do end describe 'When asking for a note on issue' do - let(:issue) { create(:issue, created_at: fixed_time, updated_at: fixed_time) } - let(:note) { create(:note_on_issue, noteable_id: issue.id, project: project) } + let(:issue) do + create(:issue, created_at: fixed_time, updated_at: fixed_time, + project: project) + end + + let(:note) do + create(:note_on_issue, noteable: issue, project: project) + end it 'returns the note and issue-specific data' do expect(data).to have_key(:issue) - expect(data[:issue].except('updated_at')).to eq(issue.hook_attrs.except('updated_at')) - expect(data[:issue]['updated_at']).to be > issue.hook_attrs['updated_at'] + expect(data[:issue].except('updated_at')) + .to eq(issue.reload.hook_attrs.except('updated_at')) + expect(data[:issue]['updated_at']) + .to be > issue.hook_attrs['updated_at'] end include_examples 'project hook data' @@ -51,13 +60,23 @@ describe 'Gitlab::NoteDataBuilder', lib: true do end describe 'When asking for a note on merge request' do - let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) } - let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id, project: project) } + let(:merge_request) do + create(:merge_request, created_at: fixed_time, + updated_at: fixed_time, + source_project: project) + end + + let(:note) do + create(:note_on_merge_request, noteable: merge_request, + project: project) + end it 'returns the note and merge request data' do expect(data).to have_key(:merge_request) - expect(data[:merge_request].except('updated_at')).to eq(merge_request.hook_attrs.except('updated_at')) - expect(data[:merge_request]['updated_at']).to be > merge_request.hook_attrs['updated_at'] + expect(data[:merge_request].except('updated_at')) + .to eq(merge_request.reload.hook_attrs.except('updated_at')) + expect(data[:merge_request]['updated_at']) + .to be > merge_request.hook_attrs['updated_at'] end include_examples 'project hook data' @@ -65,13 +84,22 @@ describe 'Gitlab::NoteDataBuilder', lib: true do end describe 'When asking for a note on merge request diff' do - let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) } - let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id, project: project) } + let(:merge_request) do + create(:merge_request, created_at: fixed_time, updated_at: fixed_time, + source_project: project) + end + + let(:note) do + create(:note_on_merge_request_diff, noteable: merge_request, + project: project) + end it 'returns the note and merge request diff data' do expect(data).to have_key(:merge_request) - expect(data[:merge_request].except('updated_at')).to eq(merge_request.hook_attrs.except('updated_at')) - expect(data[:merge_request]['updated_at']).to be > merge_request.hook_attrs['updated_at'] + expect(data[:merge_request].except('updated_at')) + .to eq(merge_request.reload.hook_attrs.except('updated_at')) + expect(data[:merge_request]['updated_at']) + .to be > merge_request.hook_attrs['updated_at'] end include_examples 'project hook data' @@ -79,13 +107,22 @@ describe 'Gitlab::NoteDataBuilder', lib: true do end describe 'When asking for a note on project snippet' do - let!(:snippet) { create(:project_snippet, created_at: fixed_time, updated_at: fixed_time) } - let!(:note) { create(:note_on_project_snippet, noteable_id: snippet.id, project: project) } + let!(:snippet) do + create(:project_snippet, created_at: fixed_time, updated_at: fixed_time, + project: project) + end + + let!(:note) do + create(:note_on_project_snippet, noteable: snippet, + project: project) + end it 'returns the note and project snippet data' do expect(data).to have_key(:snippet) - expect(data[:snippet].except('updated_at')).to eq(snippet.hook_attrs.except('updated_at')) - expect(data[:snippet]['updated_at']).to be > snippet.hook_attrs['updated_at'] + expect(data[:snippet].except('updated_at')) + .to eq(snippet.reload.hook_attrs.except('updated_at')) + expect(data[:snippet]['updated_at']) + .to be > snippet.hook_attrs['updated_at'] end include_examples 'project hook data' diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb index c2a51d9249c..84c21ceefd9 100644 --- a/spec/lib/gitlab/saml/user_spec.rb +++ b/spec/lib/gitlab/saml/user_spec.rb @@ -145,6 +145,7 @@ describe Gitlab::Saml::User, lib: true do allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) } allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) + allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user) end context 'and no account for the LDAP user' do @@ -177,6 +178,23 @@ describe Gitlab::Saml::User, lib: true do ]) end end + + context 'user has SAML user, and wants to add their LDAP identity' do + it 'adds the LDAP identity to the existing SAML user' do + create(:omniauth_user, email: 'john@mail.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'saml', username: 'john') + local_hash = OmniAuth::AuthHash.new(uid: 'uid=user1,ou=People,dc=example', provider: provider, info: info_hash) + local_saml_user = described_class.new(local_hash) + local_saml_user.save + local_gl_user = local_saml_user.gl_user + + expect(local_gl_user).to be_valid + expect(local_gl_user.identities.length).to eql 2 + identities_as_hash = local_gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } + expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'saml', extern_uid: 'uid=user1,ou=People,dc=example' } + ]) + end + end end end end diff --git a/spec/lib/gitlab/sanitizers/svg_spec.rb b/spec/lib/gitlab/sanitizers/svg_spec.rb new file mode 100644 index 00000000000..030c2063ab2 --- /dev/null +++ b/spec/lib/gitlab/sanitizers/svg_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe Gitlab::Sanitizers::SVG do + let(:scrubber) { Gitlab::Sanitizers::SVG::Scrubber.new } + let(:namespace) { double(Nokogiri::XML::Namespace, prefix: 'xlink', href: 'http://www.w3.org/1999/xlink') } + let(:namespaced_attr) { double(Nokogiri::XML::Attr, name: 'href', namespace: namespace, value: '#awesome_id') } + + describe '.clean' do + let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') } + let(:data) { open(input_svg_path).read } + let(:sanitized_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') } + let(:sanitized) { open(sanitized_svg_path).read } + + it 'delegates sanitization to scrubber' do + expect_any_instance_of(Gitlab::Sanitizers::SVG::Scrubber).to receive(:scrub).at_least(:once) + described_class.clean(data) + end + + it 'returns sanitized data' do + expect(described_class.clean(data)).to eq(sanitized) + end + end + + context 'scrubber' do + describe '#scrub' do + let(:invalid_element) { double(Nokogiri::XML::Node, name: 'invalid', value: 'invalid') } + let(:invalid_attribute) { double(Nokogiri::XML::Attr, name: 'invalid', namespace: nil) } + let(:valid_element) { double(Nokogiri::XML::Node, name: 'use') } + + it 'removes an invalid element' do + expect(invalid_element).to receive(:unlink) + + scrubber.scrub(invalid_element) + end + + it 'removes an invalid attribute' do + allow(valid_element).to receive(:attribute_nodes) { [invalid_attribute] } + expect(invalid_attribute).to receive(:unlink) + + scrubber.scrub(valid_element) + end + + it 'accepts valid element' do + allow(valid_element).to receive(:attribute_nodes) { [namespaced_attr] } + expect(valid_element).not_to receive(:unlink) + + scrubber.scrub(valid_element) + end + + it 'accepts valid namespaced attributes' do + allow(valid_element).to receive(:attribute_nodes) { [namespaced_attr] } + expect(namespaced_attr).not_to receive(:unlink) + + scrubber.scrub(valid_element) + end + end + + describe '#attribute_name_with_namespace' do + it 'returns name with prefix when attribute is namespaced' do + expect(scrubber.attribute_name_with_namespace(namespaced_attr)).to eq('xlink:href') + end + end + + describe '#unsafe_href?' do + let(:unsafe_attr) { double(Nokogiri::XML::Attr, name: 'href', namespace: namespace, value: 'http://evilsite.example.com/random.svg') } + + it 'returns true if href attribute is an external url' do + expect(scrubber.unsafe_href?(unsafe_attr)).to be_truthy + end + + it 'returns false if href atttribute is an internal reference' do + expect(scrubber.unsafe_href?(namespaced_attr)).to be_falsey + end + end + + describe '#data_attribute?' do + let(:data_attr) { double(Nokogiri::XML::Attr, name: 'data-gitlab', namespace: nil, value: 'gitlab is awesome') } + let(:namespaced_attr) { double(Nokogiri::XML::Attr, name: 'data-gitlab', namespace: namespace, value: 'gitlab is awesome') } + let(:other_attr) { double(Nokogiri::XML::Attr, name: 'something', namespace: nil, value: 'content') } + + it 'returns true if is a valid data attribute' do + expect(scrubber.data_attribute?(data_attr)).to be_truthy + end + + it 'returns false if attribute is namespaced' do + expect(scrubber.data_attribute?(namespaced_attr)).to be_falsey + end + + it 'returns false if not a data attribute' do + expect(scrubber.data_attribute?(other_attr)).to be_falsey + end + end + end +end diff --git a/spec/lib/gitlab/sherlock/collection_spec.rb b/spec/lib/gitlab/sherlock/collection_spec.rb index de6bb86c5dd..2ae79b50e77 100644 --- a/spec/lib/gitlab/sherlock/collection_spec.rb +++ b/spec/lib/gitlab/sherlock/collection_spec.rb @@ -11,13 +11,13 @@ describe Gitlab::Sherlock::Collection, lib: true do it 'adds a new transaction' do collection.add(transaction) - expect(collection).to_not be_empty + expect(collection).not_to be_empty end it 'is aliased as <<' do collection << transaction - expect(collection).to_not be_empty + expect(collection).not_to be_empty end end @@ -47,7 +47,7 @@ describe Gitlab::Sherlock::Collection, lib: true do it 'returns false for a collection with a transaction' do collection.add(transaction) - expect(collection).to_not be_empty + expect(collection).not_to be_empty end end diff --git a/spec/lib/gitlab/sherlock/query_spec.rb b/spec/lib/gitlab/sherlock/query_spec.rb index 05da915ccfd..0a620428138 100644 --- a/spec/lib/gitlab/sherlock/query_spec.rb +++ b/spec/lib/gitlab/sherlock/query_spec.rb @@ -85,7 +85,7 @@ FROM users; frames = query.application_backtrace expect(frames).to be_an_instance_of(Array) - expect(frames).to_not be_empty + expect(frames).not_to be_empty frames.each do |frame| expect(frame.path).to start_with(Rails.root.to_s) diff --git a/spec/lib/gitlab/sherlock/transaction_spec.rb b/spec/lib/gitlab/sherlock/transaction_spec.rb index 7553f2a045f..9fe18f253f0 100644 --- a/spec/lib/gitlab/sherlock/transaction_spec.rb +++ b/spec/lib/gitlab/sherlock/transaction_spec.rb @@ -203,7 +203,7 @@ describe Gitlab::Sherlock::Transaction, lib: true do end it 'only tracks queries triggered from the transaction thread' do - expect(transaction).to_not receive(:track_query) + expect(transaction).not_to receive(:track_query) Thread.new { subscription.publish('test', time, time, nil, query_data) }. join @@ -226,7 +226,7 @@ describe Gitlab::Sherlock::Transaction, lib: true do end it 'only tracks views rendered from the transaction thread' do - expect(transaction).to_not receive(:track_view) + expect(transaction).not_to receive(:track_view) Thread.new { subscription.publish('test', time, time, nil, view_data) }. join diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb new file mode 100644 index 00000000000..de55334118f --- /dev/null +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe Gitlab::UrlSanitizer, lib: true do + let(:credentials) { { user: 'blah', password: 'password' } } + let(:url_sanitizer) do + described_class.new("https://github.com/me/project.git", credentials: credentials) + end + + describe '.sanitize' do + def sanitize_url(url) + # We want to try with multi-line content because is how error messages are formatted + described_class.sanitize(%Q{ + remote: Not Found + fatal: repository '#{url}' not found + }) + end + + it 'mask the credentials from HTTP URLs' do + filtered_content = sanitize_url('http://user:pass@test.com/root/repoC.git/') + + expect(filtered_content).to include("http://*****:*****@test.com/root/repoC.git/") + end + + it 'mask the credentials from HTTPS URLs' do + filtered_content = sanitize_url('https://user:pass@test.com/root/repoA.git/') + + expect(filtered_content).to include("https://*****:*****@test.com/root/repoA.git/") + end + + it 'mask credentials from SSH URLs' do + filtered_content = sanitize_url('ssh://user@host.test/path/to/repo.git') + + expect(filtered_content).to include("ssh://*****@host.test/path/to/repo.git") + end + + it 'does not modify Git URLs' do + # git protocol does not support authentication + filtered_content = sanitize_url('git://host.test/path/to/repo.git') + + expect(filtered_content).to include("git://host.test/path/to/repo.git") + end + + it 'does not modify scp-like URLs' do + filtered_content = sanitize_url('user@server:project.git') + + expect(filtered_content).to include("user@server:project.git") + end + end + + describe '#sanitized_url' do + it { expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") } + end + + describe '#credentials' do + it { expect(url_sanitizer.credentials).to eq(credentials) } + end + + describe '#full_url' do + it { expect(url_sanitizer.full_url).to eq("https://blah:password@github.com/me/project.git") } + + it 'supports scp-like URLs' do + sanitizer = described_class.new('user@server:project.git') + + expect(sanitizer.full_url).to eq('user@server:project.git') + end + end + +end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index d940bf05061..c5c1402e8fc 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -11,7 +11,7 @@ describe Gitlab::Workhorse, lib: true do end it "raises an error" do - expect { subject.send_git_archive(project, "master", "zip") }.to raise_error(RuntimeError) + expect { subject.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError) end end end diff --git a/spec/lib/json_web_token/rsa_token_spec.rb b/spec/lib/json_web_token/rsa_token_spec.rb new file mode 100644 index 00000000000..18726754517 --- /dev/null +++ b/spec/lib/json_web_token/rsa_token_spec.rb @@ -0,0 +1,43 @@ +describe JSONWebToken::RSAToken do + let(:rsa_key) do + OpenSSL::PKey::RSA.new <<-eos.strip_heredoc + -----BEGIN RSA PRIVATE KEY----- + MIIBOgIBAAJBAMA5sXIBE0HwgIB40iNidN4PGWzOyLQK0bsdOBNgpEXkDlZBvnak + OUgAPF+rME4PB0Yl415DabUI40T5UNmlwxcCAwEAAQJAZtY2pSwIFm3JAXIh0cZZ + iXcAfiJ+YzuqinUOS+eW2sBCAEzjcARlU/o6sFQgtsOi4FOMczAd1Yx8UDMXMmrw + 2QIhAPBgVhJiTF09pdmeFWutCvTJDlFFAQNbrbo2X2x/9WF9AiEAzLgqMKeStSRu + H9N16TuDrUoO8R+DPqriCwkKrSHaWyMCIFzMhE4inuKcSywBaLmiG4m3GQzs++Al + A6PRG/PSTpQtAiBxtBg6zdf+JC3GH3zt/dA0/10tL4OF2wORfYQghRzyYQIhAL2l + 0ZQW+yLIZAGrdBFWYEAa52GZosncmzBNlsoTgwE4 + -----END RSA PRIVATE KEY----- + eos + end + let(:rsa_token) { described_class.new(nil) } + let(:rsa_encoded) { rsa_token.encoded } + + before { allow_any_instance_of(described_class).to receive(:key).and_return(rsa_key) } + + context 'token' do + context 'for valid key to be validated' do + before { rsa_token['key'] = 'value' } + + subject { JWT.decode(rsa_encoded, rsa_key) } + + it { expect{subject}.not_to raise_error } + it { expect(subject.first).to include('key' => 'value') } + it do + expect(subject.second).to eq( + "typ" => "JWT", + "alg" => "RS256", + "kid" => "OGXY:4TR7:FAVO:WEM2:XXEW:E4FP:TKL7:7ACK:TZAF:D54P:SUIA:P3B2") + end + end + + context 'for invalid key to raise an exception' do + let(:new_key) { OpenSSL::PKey::RSA.generate(512) } + subject { JWT.decode(rsa_encoded, new_key) } + + it { expect{subject}.to raise_error(JWT::DecodeError) } + end + end +end diff --git a/spec/lib/json_web_token/token_spec.rb b/spec/lib/json_web_token/token_spec.rb new file mode 100644 index 00000000000..3d955e4d774 --- /dev/null +++ b/spec/lib/json_web_token/token_spec.rb @@ -0,0 +1,18 @@ +describe JSONWebToken::Token do + let(:token) { described_class.new } + + context 'custom parameters' do + let(:value) { 'value' } + before { token[:key] = value } + + it { expect(token[:key]).to eq(value) } + it { expect(token.payload).to include(key: value) } + end + + context 'embeds default payload' do + subject { token.payload } + let(:default) { token.send(:default_payload) } + + it { is_expected.to include(default) } + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 495c5cbac00..818825b1477 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -51,7 +51,7 @@ describe Notify do context 'when enabled email_author_in_body' do before do - allow(current_application_settings).to receive(:email_author_in_body).and_return(true) + allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true) end it 'contains a link to note author' do @@ -230,7 +230,7 @@ describe Notify do context 'when enabled email_author_in_body' do before do - allow(current_application_settings).to receive(:email_author_in_body).and_return(true) + allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true) end it 'contains a link to note author' do @@ -454,7 +454,7 @@ describe Notify do context 'when enabled email_author_in_body' do before do - allow(current_application_settings).to receive(:email_author_in_body).and_return(true) + allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true) end it 'contains a link to note author' do @@ -593,7 +593,7 @@ describe Notify do let(:user) { create(:user) } let(:tree_path) { namespace_project_tree_path(project.namespace, project, "master") } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :create) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -606,10 +606,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /Pushed new branch master/ end @@ -624,7 +620,7 @@ describe Notify do let(:user) { create(:user) } let(:tree_path) { namespace_project_tree_path(project.namespace, project, "v1.0") } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :create) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -637,10 +633,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /Pushed new tag v1\.0/ end @@ -654,7 +646,7 @@ describe Notify do let(:example_site_path) { root_path } let(:user) { create(:user) } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :delete) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -667,10 +659,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /Deleted branch master/ end @@ -680,7 +668,7 @@ describe Notify do let(:example_site_path) { root_path } let(:user) { create(:user) } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -693,10 +681,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /Deleted tag v1\.0/ end @@ -709,8 +693,9 @@ describe Notify do let(:commits) { Commit.decorate(compare.commits, nil) } let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) } let(:send_from_committer_email) { false } + let(:diff_refs) { [project.merge_base_commit(sample_image_commit.id, sample_commit.id), project.commit(sample_commit.id)] } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, diff_refs: diff_refs, send_from_committer_email: send_from_committer_email) } it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" @@ -723,10 +708,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /\[#{project.path_with_namespace}\]\[master\] #{commits.length} commits:/ end @@ -735,15 +716,15 @@ describe Notify do is_expected.to have_body_text /Change some files/ end - it 'includes diffs' do - is_expected.to have_body_text /def archive_formats_regex/ + it 'includes diffs with character-level highlighting' do + is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/ end it 'contains a link to the diff' do is_expected.to have_body_text /#{diff_path}/ end - it 'doesn not contain the misleading footer' do + it 'does not contain the misleading footer' do is_expected.not_to have_body_text /you are a member of/ end @@ -817,8 +798,9 @@ describe Notify do let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) } let(:commits) { Commit.decorate(compare.commits, nil) } let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) } + let(:diff_refs) { [project.merge_base_commit(sample_commit.parent_id, sample_commit.id), project.commit(sample_commit.id)] } - subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) } + subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, diff_refs: diff_refs) } it_behaves_like 'it should show Gmail Actions View Commit link' it_behaves_like "a user cannot unsubscribe through footer link" @@ -831,10 +813,6 @@ describe Notify do expect(sender.address).to eq(gitlab_sender) end - it 'is sent to recipient' do - is_expected.to deliver_to 'devs@company.name' - end - it 'has the correct subject' do is_expected.to have_subject /#{commits.first.title}/ end @@ -843,8 +821,8 @@ describe Notify do is_expected.to have_body_text /Change some files/ end - it 'includes diffs' do - is_expected.to have_body_text /def archive_formats_regex/ + it 'includes diffs with character-level highlighting' do + is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/ end it 'contains a link to the diff' do diff --git a/spec/mailers/previews/devise_mailer_preview.rb b/spec/mailers/previews/devise_mailer_preview.rb new file mode 100644 index 00000000000..dc3062a4332 --- /dev/null +++ b/spec/mailers/previews/devise_mailer_preview.rb @@ -0,0 +1,11 @@ +class DeviseMailerPreview < ActionMailer::Preview + def confirmation_instructions_for_signup + user = User.new(name: 'Jane Doe', email: 'signup@example.com') + DeviseMailer.confirmation_instructions(user, 'faketoken', {}) + end + + def confirmation_instructions_for_new_email + user = User.last + DeviseMailer.confirmation_instructions(user, 'faketoken', {}) + end +end diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb index 5a85cb501dd..93de5850ba2 100644 --- a/spec/mailers/shared/notify.rb +++ b/spec/mailers/shared/notify.rb @@ -146,8 +146,8 @@ shared_examples 'it should have Gmail Actions links' do end shared_examples 'it should not have Gmail Actions links' do - it { is_expected.to_not have_body_text '<script type="application/ld+json">' } - it { is_expected.to_not have_body_text /ViewAction/ } + it { is_expected.not_to have_body_text '<script type="application/ld+json">' } + it { is_expected.not_to have_body_text /ViewAction/ } end shared_examples 'it should show Gmail Actions View Issue link' do diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb new file mode 100644 index 00000000000..1acb5846fcf --- /dev/null +++ b/spec/models/ability_spec.rb @@ -0,0 +1,117 @@ +require 'spec_helper' + +describe Ability, lib: true do + describe '.users_that_can_read_project' do + context 'using a public project' do + it 'returns all the users' do + project = create(:project, :public) + user = build(:user) + + expect(described_class.users_that_can_read_project([user], project)). + to eq([user]) + end + end + + context 'using an internal project' do + let(:project) { create(:project, :internal) } + + it 'returns users that are administrators' do + user = build(:user, admin: true) + + expect(described_class.users_that_can_read_project([user], project)). + to eq([user]) + end + + it 'returns internal users while skipping external users' do + user1 = build(:user) + user2 = build(:user, external: true) + users = [user1, user2] + + expect(described_class.users_that_can_read_project(users, project)). + to eq([user1]) + end + + it 'returns external users if they are the project owner' do + user1 = build(:user, external: true) + user2 = build(:user, external: true) + users = [user1, user2] + + expect(project).to receive(:owner).twice.and_return(user1) + + expect(described_class.users_that_can_read_project(users, project)). + to eq([user1]) + end + + it 'returns external users if they are project members' do + user1 = build(:user, external: true) + user2 = build(:user, external: true) + users = [user1, user2] + + expect(project.team).to receive(:members).twice.and_return([user1]) + + expect(described_class.users_that_can_read_project(users, project)). + to eq([user1]) + end + + it 'returns an empty Array if all users are external users without access' do + user1 = build(:user, external: true) + user2 = build(:user, external: true) + users = [user1, user2] + + expect(described_class.users_that_can_read_project(users, project)). + to eq([]) + end + end + + context 'using a private project' do + let(:project) { create(:project, :private) } + + it 'returns users that are administrators' do + user = build(:user, admin: true) + + expect(described_class.users_that_can_read_project([user], project)). + to eq([user]) + end + + it 'returns external users if they are the project owner' do + user1 = build(:user, external: true) + user2 = build(:user, external: true) + users = [user1, user2] + + expect(project).to receive(:owner).twice.and_return(user1) + + expect(described_class.users_that_can_read_project(users, project)). + to eq([user1]) + end + + it 'returns external users if they are project members' do + user1 = build(:user, external: true) + user2 = build(:user, external: true) + users = [user1, user2] + + expect(project.team).to receive(:members).twice.and_return([user1]) + + expect(described_class.users_that_can_read_project(users, project)). + to eq([user1]) + end + + it 'returns an empty Array if all users are internal users without access' do + user1 = build(:user) + user2 = build(:user) + users = [user1, user2] + + expect(described_class.users_that_can_read_project(users, project)). + to eq([]) + end + + it 'returns an empty Array if all users are external users without access' do + user1 = build(:user, external: true) + user2 = build(:user, external: true) + users = [user1, user2] + + expect(described_class.users_that_can_read_project(users, project)). + to eq([]) + end + end + end +end diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index ac12ab6c757..305f8bc88cc 100644 --- a/spec/models/abuse_report_spec.rb +++ b/spec/models/abuse_report_spec.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: abuse_reports -# -# id :integer not null, primary key -# reporter_id :integer -# user_id :integer -# message :text -# created_at :datetime -# updated_at :datetime -# - require 'rails_helper' RSpec.describe AbuseReport, type: :model do diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 520cf1b75de..d84f3e998f5 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -1,49 +1,3 @@ -# == Schema Information -# -# Table name: application_settings -# -# id :integer not null, primary key -# default_projects_limit :integer -# signup_enabled :boolean -# signin_enabled :boolean -# gravatar_enabled :boolean -# sign_in_text :text -# created_at :datetime -# updated_at :datetime -# home_page_url :string(255) -# default_branch_protection :integer default(2) -# restricted_visibility_levels :text -# version_check_enabled :boolean default(TRUE) -# max_attachment_size :integer default(10), not null -# default_project_visibility :integer -# default_snippet_visibility :integer -# restricted_signup_domains :text -# user_oauth_applications :boolean default(TRUE) -# after_sign_out_path :string(255) -# session_expire_delay :integer default(10080), not null -# import_sources :text -# help_page_text :text -# admin_notification_email :string(255) -# shared_runners_enabled :boolean default(TRUE), not null -# max_artifacts_size :integer default(100), not null -# runners_registration_token :string -# require_two_factor_authentication :boolean default(FALSE) -# two_factor_grace_period :integer default(48) -# metrics_enabled :boolean default(FALSE) -# metrics_host :string default("localhost") -# metrics_username :string -# metrics_password :string -# metrics_pool_size :integer default(16) -# metrics_timeout :integer default(10) -# metrics_method_call_threshold :integer default(10) -# recaptcha_enabled :boolean default(FALSE) -# recaptcha_site_key :string -# recaptcha_private_key :string -# metrics_port :integer default(8089) -# sentry_enabled :boolean default(FALSE) -# sentry_dsn :string -# - require 'spec_helper' describe ApplicationSetting, models: true do @@ -66,6 +20,15 @@ describe ApplicationSetting, models: true do it { is_expected.to allow_value(https).for(:after_sign_out_path) } it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) } + describe 'disabled_oauth_sign_in_sources validations' do + before do + allow(Devise).to receive(:omniauth_providers).and_return([:github]) + end + + it { is_expected.to allow_value(['github']).for(:disabled_oauth_sign_in_sources) } + it { is_expected.not_to allow_value(['test']).for(:disabled_oauth_sign_in_sources) } + end + it { is_expected.to validate_presence_of(:max_attachment_size) } it do diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb new file mode 100644 index 00000000000..cb3c592f8cd --- /dev/null +++ b/spec/models/award_emoji_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe AwardEmoji, models: true do + describe 'Associations' do + it { is_expected.to belong_to(:awardable) } + it { is_expected.to belong_to(:user) } + end + + describe 'modules' do + it { is_expected.to include_module(Participable) } + end + + describe "validations" do + it { is_expected.to validate_presence_of(:awardable) } + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:name) } + + # To circumvent a bug in the shoulda matchers + describe "scoped uniqueness validation" do + it "rejects duplicate award emoji" do + user = create(:user) + issue = create(:issue) + create(:award_emoji, user: user, awardable: issue) + new_award = build(:award_emoji, user: user, awardable: issue) + + expect(new_award).not_to be_valid + end + end + end +end diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index f6f84db57e6..6ad8bfef4f2 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -1,17 +1,3 @@ -# == Schema Information -# -# Table name: broadcast_messages -# -# id :integer not null, primary key -# message :text not null -# starts_at :datetime -# ends_at :datetime -# created_at :datetime -# updated_at :datetime -# color :string(255) -# font :string(255) -# - require 'spec_helper' describe BroadcastMessage, models: true do diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index b5d356aa066..2beb6cc598d 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -1,18 +1,17 @@ require 'spec_helper' describe Ci::Build, models: true do - let(:project) { FactoryGirl.create :project } - let(:commit) { FactoryGirl.create :ci_commit, project: project } - let(:build) { FactoryGirl.create :ci_build, commit: commit } + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } it { is_expected.to validate_presence_of :ref } it { is_expected.to respond_to :trace_html } describe '#first_pending' do - let(:first) { FactoryGirl.create :ci_build, commit: commit, status: 'pending', created_at: Date.yesterday } - let(:second) { FactoryGirl.create :ci_build, commit: commit, status: 'pending' } - before { first; second } + let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) } + let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') } subject { Ci::Build.first_pending } it { is_expected.to be_a(Ci::Build) } @@ -90,7 +89,7 @@ describe Ci::Build, models: true do build.update_attributes(trace: token) end - it { is_expected.to_not include(token) } + it { is_expected.not_to include(token) } end end @@ -98,7 +97,7 @@ describe Ci::Build, models: true do # describe :timeout do # subject { build.timeout } # - # it { is_expected.to eq(commit.project.timeout) } + # it { is_expected.to eq(pipeline.project.timeout) } # end describe '#options' do @@ -125,13 +124,13 @@ describe Ci::Build, models: true do describe '#project' do subject { build.project } - it { is_expected.to eq(commit.project) } + it { is_expected.to eq(pipeline.project) } end describe '#project_id' do subject { build.project_id } - it { is_expected.to eq(commit.project_id) } + it { is_expected.to eq(pipeline.project_id) } end describe '#project_name' do @@ -219,8 +218,8 @@ describe Ci::Build, models: true do it { is_expected.to eq(predefined_variables + yaml_variables + secure_variables) } context 'and trigger variables' do - let(:trigger) { FactoryGirl.create :ci_trigger, project: project } - let(:trigger_request) { FactoryGirl.create :ci_trigger_request_with_variables, commit: commit, trigger: trigger } + let(:trigger) { create(:ci_trigger, project: project) } + let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) } let(:trigger_variables) do [ { key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false } @@ -259,11 +258,11 @@ describe Ci::Build, models: true do end describe '#can_be_served?' do - let(:runner) { FactoryGirl.create :ci_runner } + let(:runner) { create(:ci_runner) } before { build.project.runners << runner } - context 'runner without tags' do + context 'when runner does not have tags' do it 'can handle builds without tags' do expect(build.can_be_served?(runner)).to be_truthy end @@ -274,25 +273,53 @@ describe Ci::Build, models: true do end end - context 'runner with tags' do + context 'when runner has tags' do before { runner.tag_list = ['bb', 'cc'] } - it 'can handle builds without tags' do - expect(build.can_be_served?(runner)).to be_truthy + shared_examples 'tagged build picker' do + it 'can handle build with matching tags' do + build.tag_list = ['bb'] + expect(build.can_be_served?(runner)).to be_truthy + end + + it 'cannot handle build without matching tags' do + build.tag_list = ['aa'] + expect(build.can_be_served?(runner)).to be_falsey + end end - it 'can handle build with matching tags' do - build.tag_list = ['bb'] - expect(build.can_be_served?(runner)).to be_truthy + context 'when runner can pick untagged jobs' do + it 'can handle builds without tags' do + expect(build.can_be_served?(runner)).to be_truthy + end + + it_behaves_like 'tagged build picker' end - it 'cannot handle build with not matching tags' do - build.tag_list = ['aa'] - expect(build.can_be_served?(runner)).to be_falsey + context 'when runner can not pick untagged jobs' do + before { runner.run_untagged = false } + + it 'can not handle builds without tags' do + expect(build.can_be_served?(runner)).to be_falsey + end + + it_behaves_like 'tagged build picker' end end end + describe '#has_tags?' do + context 'when build has tags' do + subject { create(:ci_build, tag_list: ['tag']) } + it { is_expected.to have_tags } + end + + context 'when build does not have tags' do + subject { create(:ci_build, tag_list: []) } + it { is_expected.not_to have_tags } + end + end + describe '#any_runners_online?' do subject { build.any_runners_online? } @@ -301,7 +328,7 @@ describe Ci::Build, models: true do end context 'if there are runner' do - let(:runner) { FactoryGirl.create :ci_runner } + let(:runner) { create(:ci_runner) } before do build.project.runners << runner @@ -338,7 +365,7 @@ describe Ci::Build, models: true do it { is_expected.to be_truthy } context "and there are specific runner" do - let(:runner) { FactoryGirl.create :ci_runner, contacted_at: 1.second.ago } + let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) } before do build.project.runners << runner @@ -387,7 +414,7 @@ describe Ci::Build, models: true do end describe '#repo_url' do - let(:build) { FactoryGirl.create :ci_build } + let(:build) { create(:ci_build) } let(:project) { build.project } subject { build.repo_url } @@ -401,10 +428,10 @@ describe Ci::Build, models: true do end describe '#depends_on_builds' do - let!(:build) { FactoryGirl.create :ci_build, commit: commit, name: 'build', stage_idx: 0, stage: 'build' } - let!(:rspec_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rspec', stage_idx: 1, stage: 'test' } - let!(:rubocop_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rubocop', stage_idx: 1, stage: 'test' } - let!(:staging) { FactoryGirl.create :ci_build, commit: commit, name: 'staging', stage_idx: 2, stage: 'deploy' } + let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') } + let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') } + let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') } + let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') } it 'to have no dependents if this is first build' do expect(build.depends_on_builds).to be_empty @@ -424,20 +451,19 @@ describe Ci::Build, models: true do end end - def create_mr(build, commit, factory: :merge_request, created_at: Time.now) - FactoryGirl.create(factory, - source_project_id: commit.gl_project_id, - target_project_id: commit.gl_project_id, - source_branch: build.ref, - created_at: created_at) + def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now) + create(factory, source_project_id: pipeline.gl_project_id, + target_project_id: pipeline.gl_project_id, + source_branch: build.ref, + created_at: created_at) end describe '#merge_request' do - context 'when a MR has a reference to the commit' do + context 'when a MR has a reference to the pipeline' do before do - @merge_request = create_mr(build, commit, factory: :merge_request) + @merge_request = create_mr(build, pipeline, factory: :merge_request) - commits = [double(id: commit.sha)] + commits = [double(id: pipeline.sha)] allow(@merge_request).to receive(:commits).and_return(commits) allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) end @@ -447,19 +473,19 @@ describe Ci::Build, models: true do end end - context 'when there is not a MR referencing the commit' do + context 'when there is not a MR referencing the pipeline' do it 'returns nil' do expect(build.merge_request).to be_nil end end - context 'when more than one MR have a reference to the commit' do + context 'when more than one MR have a reference to the pipeline' do before do - @merge_request = create_mr(build, commit, factory: :merge_request) + @merge_request = create_mr(build, pipeline, factory: :merge_request) @merge_request.close! - @merge_request2 = create_mr(build, commit, factory: :merge_request) + @merge_request2 = create_mr(build, pipeline, factory: :merge_request) - commits = [double(id: commit.sha)] + commits = [double(id: pipeline.sha)] allow(@merge_request).to receive(:commits).and_return(commits) allow(@merge_request2).to receive(:commits).and_return(commits) allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request, @merge_request2]) @@ -472,11 +498,11 @@ describe Ci::Build, models: true do context 'when a Build is created after the MR' do before do - @merge_request = create_mr(build, commit, factory: :merge_request_with_diffs) - commit2 = FactoryGirl.create :ci_commit, project: project - @build2 = FactoryGirl.create :ci_build, commit: commit2 + @merge_request = create_mr(build, pipeline, factory: :merge_request_with_diffs) + pipeline2 = create(:ci_pipeline, project: project) + @build2 = create(:ci_build, pipeline: pipeline2) - commits = [double(id: commit.sha), double(id: commit2.sha)] + commits = [double(id: pipeline.sha), double(id: pipeline2.sha)] allow(@merge_request).to receive(:commits).and_return(commits) allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) end @@ -506,7 +532,7 @@ describe Ci::Build, models: true do end it 'should set erase date' do - expect(build.erased_at).to_not be_falsy + expect(build.erased_at).not_to be_falsy end end @@ -578,7 +604,7 @@ describe Ci::Build, models: true do describe '#erase' do it 'should not raise error' do - expect { build.erase }.to_not raise_error + expect { build.erase }.not_to raise_error end end end diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb deleted file mode 100644 index a747aa08447..00000000000 --- a/spec/models/ci/commit_spec.rb +++ /dev/null @@ -1,422 +0,0 @@ -# == Schema Information -# -# Table name: ci_commits -# -# id :integer not null, primary key -# project_id :integer -# ref :string(255) -# sha :string(255) -# before_sha :string(255) -# push_data :text -# created_at :datetime -# updated_at :datetime -# tag :boolean default(FALSE) -# yaml_errors :text -# committed_at :datetime -# gl_project_id :integer -# - -require 'spec_helper' - -describe Ci::Commit, models: true do - let(:project) { FactoryGirl.create :empty_project } - let(:commit) { FactoryGirl.create :ci_commit, project: project } - - it { is_expected.to belong_to(:project) } - it { is_expected.to have_many(:statuses) } - it { is_expected.to have_many(:trigger_requests) } - it { is_expected.to have_many(:builds) } - it { is_expected.to validate_presence_of :sha } - it { is_expected.to validate_presence_of :status } - it { is_expected.to delegate_method(:stages).to(:statuses) } - - it { is_expected.to respond_to :git_author_name } - it { is_expected.to respond_to :git_author_email } - it { is_expected.to respond_to :short_sha } - - describe :valid_commit_sha do - context 'commit.sha can not start with 00000000' do - before do - commit.sha = '0' * 40 - commit.valid_commit_sha - end - - it('commit errors should not be empty') { expect(commit.errors).not_to be_empty } - end - end - - describe :short_sha do - subject { commit.short_sha } - - it 'has 8 items' do - expect(subject.size).to eq(8) - end - it { expect(commit.sha).to start_with(subject) } - end - - describe :create_next_builds do - end - - describe :retried do - subject { commit.retried } - - before do - @commit1 = FactoryGirl.create :ci_build, commit: commit, name: 'deploy' - @commit2 = FactoryGirl.create :ci_build, commit: commit, name: 'deploy' - end - - it 'returns old builds' do - is_expected.to contain_exactly(@commit1) - end - end - - describe :create_builds do - let!(:commit) { FactoryGirl.create :ci_commit, project: project, ref: 'master', tag: false } - - def create_builds(trigger_request = nil) - commit.create_builds(nil, trigger_request) - end - - def create_next_builds - commit.create_next_builds(commit.builds.order(:id).last) - end - - it 'creates builds' do - expect(create_builds).to be_truthy - commit.builds.update_all(status: "success") - expect(commit.builds.count(:all)).to eq(2) - - expect(create_next_builds).to be_truthy - commit.builds.update_all(status: "success") - expect(commit.builds.count(:all)).to eq(4) - - expect(create_next_builds).to be_truthy - commit.builds.update_all(status: "success") - expect(commit.builds.count(:all)).to eq(5) - - expect(create_next_builds).to be_falsey - end - - context 'custom stage with first job allowed to fail' do - let(:yaml) do - { - stages: ['clean', 'test'], - clean_job: { - stage: 'clean', - allow_failure: true, - script: 'BUILD', - }, - test_job: { - stage: 'test', - script: 'TEST', - }, - } - end - - before do - stub_ci_commit_yaml_file(YAML.dump(yaml)) - create_builds - end - - it 'properly schedules builds' do - expect(commit.builds.pluck(:status)).to contain_exactly('pending') - commit.builds.running_or_pending.each(&:drop) - expect(commit.builds.pluck(:status)).to contain_exactly('pending', 'failed') - end - end - - context 'properly creates builds when "when" is defined' do - let(:yaml) do - { - stages: ["build", "test", "test_failure", "deploy", "cleanup"], - build: { - stage: "build", - script: "BUILD", - }, - test: { - stage: "test", - script: "TEST", - }, - test_failure: { - stage: "test_failure", - script: "ON test failure", - when: "on_failure", - }, - deploy: { - stage: "deploy", - script: "PUBLISH", - }, - cleanup: { - stage: "cleanup", - script: "TIDY UP", - when: "always", - } - } - end - - before do - stub_ci_commit_yaml_file(YAML.dump(yaml)) - end - - context 'when builds are successful' do - it 'properly creates builds' do - expect(create_builds).to be_truthy - expect(commit.builds.pluck(:name)).to contain_exactly('build') - expect(commit.builds.pluck(:status)).to contain_exactly('pending') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success') - commit.reload - expect(commit.status).to eq('success') - end - end - - context 'when test job fails' do - it 'properly creates builds' do - expect(create_builds).to be_truthy - expect(commit.builds.pluck(:name)).to contain_exactly('build') - expect(commit.builds.pluck(:status)).to contain_exactly('pending') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') - commit.builds.running_or_pending.each(&:drop) - - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success') - commit.reload - expect(commit.status).to eq('failed') - end - end - - context 'when test and test_failure jobs fail' do - it 'properly creates builds' do - expect(create_builds).to be_truthy - expect(commit.builds.pluck(:name)).to contain_exactly('build') - expect(commit.builds.pluck(:status)).to contain_exactly('pending') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') - commit.builds.running_or_pending.each(&:drop) - - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') - commit.builds.running_or_pending.each(&:drop) - - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success') - commit.reload - expect(commit.status).to eq('failed') - end - end - - context 'when deploy job fails' do - it 'properly creates builds' do - expect(create_builds).to be_truthy - expect(commit.builds.pluck(:name)).to contain_exactly('build') - expect(commit.builds.pluck(:status)).to contain_exactly('pending') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') - commit.builds.running_or_pending.each(&:drop) - - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success') - commit.reload - expect(commit.status).to eq('failed') - end - end - - context 'when build is canceled in the second stage' do - it 'does not schedule builds after build has been canceled' do - expect(create_builds).to be_truthy - expect(commit.builds.pluck(:name)).to contain_exactly('build') - expect(commit.builds.pluck(:status)).to contain_exactly('pending') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.running_or_pending).to_not be_empty - - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') - commit.builds.running_or_pending.each(&:cancel) - - expect(commit.builds.running_or_pending).to be_empty - expect(commit.reload.status).to eq('canceled') - end - end - end - end - - describe "#finished_at" do - let(:commit) { FactoryGirl.create :ci_commit } - - it "returns finished_at of latest build" do - build = FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 60 - FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 120 - - expect(commit.finished_at.to_i).to eq(build.finished_at.to_i) - end - - it "returns nil if there is no finished build" do - FactoryGirl.create :ci_not_started_build, commit: commit - - expect(commit.finished_at).to be_nil - end - end - - describe "coverage" do - let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" } - let(:commit) { FactoryGirl.create :ci_commit, project: project } - - it "calculates average when there are two builds with coverage" do - FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit - FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit - expect(commit.coverage).to eq("35.00") - end - - it "calculates average when there are two builds with coverage and one with nil" do - FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit - FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit - FactoryGirl.create :ci_build, commit: commit - expect(commit.coverage).to eq("35.00") - end - - it "calculates average when there are two builds with coverage and one is retried" do - FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit - FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, commit: commit - FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit - expect(commit.coverage).to eq("35.00") - end - - it "calculates average when there is one build without coverage" do - FactoryGirl.create :ci_build, commit: commit - expect(commit.coverage).to be_nil - end - end - - describe '#retryable?' do - subject { commit.retryable? } - - context 'no failed builds' do - before do - FactoryGirl.create :ci_build, name: "rspec", commit: commit, status: 'success' - end - - it 'be not retryable' do - is_expected.to be_falsey - end - end - - context 'with failed builds' do - before do - FactoryGirl.create :ci_build, name: "rspec", commit: commit, status: 'running' - FactoryGirl.create :ci_build, name: "rubocop", commit: commit, status: 'failed' - end - - it 'be retryable' do - is_expected.to be_truthy - end - end - end - - describe '#stages' do - let(:commit2) { FactoryGirl.create :ci_commit, project: project } - subject { CommitStatus.where(commit: [commit, commit2]).stages } - - before do - FactoryGirl.create :ci_build, commit: commit2, stage: 'test', stage_idx: 1 - FactoryGirl.create :ci_build, commit: commit, stage: 'build', stage_idx: 0 - end - - it 'return all stages' do - is_expected.to eq(%w(build test)) - end - end - - describe '#update_state' do - it 'execute update_state after touching object' do - expect(commit).to receive(:update_state).and_return(true) - commit.touch - end - - context 'dependent objects' do - let(:commit_status) { build :commit_status, commit: commit } - - it 'execute update_state after saving dependent object' do - expect(commit).to receive(:update_state).and_return(true) - commit_status.save - end - end - - context 'update state' do - let(:current) { Time.now.change(usec: 0) } - let(:build) { FactoryGirl.create :ci_build, :success, commit: commit, started_at: current - 120, finished_at: current - 60 } - - before do - build - end - - [:status, :started_at, :finished_at, :duration].each do |param| - it "update #{param}" do - expect(commit.send(param)).to eq(build.send(param)) - end - end - end - end - - describe '#branch?' do - subject { commit.branch? } - - context 'is not a tag' do - before do - commit.tag = false - end - - it 'return true when tag is set to false' do - is_expected.to be_truthy - end - end - - context 'is not a tag' do - before do - commit.tag = true - end - - it 'return false when tag is set to true' do - is_expected.to be_falsey - end - end - end -end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb new file mode 100644 index 00000000000..0d769ed7324 --- /dev/null +++ b/spec/models/ci/pipeline_spec.rb @@ -0,0 +1,403 @@ +require 'spec_helper' + +describe Ci::Pipeline, models: true do + let(:project) { FactoryGirl.create :empty_project } + let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } + + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:statuses) } + it { is_expected.to have_many(:trigger_requests) } + it { is_expected.to have_many(:builds) } + it { is_expected.to validate_presence_of :sha } + it { is_expected.to validate_presence_of :status } + + it { is_expected.to respond_to :git_author_name } + it { is_expected.to respond_to :git_author_email } + it { is_expected.to respond_to :short_sha } + + describe :valid_commit_sha do + context 'commit.sha can not start with 00000000' do + before do + pipeline.sha = '0' * 40 + pipeline.valid_commit_sha + end + + it('commit errors should not be empty') { expect(pipeline.errors).not_to be_empty } + end + end + + describe :short_sha do + subject { pipeline.short_sha } + + it 'has 8 items' do + expect(subject.size).to eq(8) + end + it { expect(pipeline.sha).to start_with(subject) } + end + + describe :create_next_builds do + end + + describe :retried do + subject { pipeline.retried } + + before do + @build1 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy' + @build2 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy' + end + + it 'returns old builds' do + is_expected.to contain_exactly(@build1) + end + end + + describe :create_builds do + let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project, ref: 'master', tag: false } + + def create_builds(trigger_request = nil) + pipeline.create_builds(nil, trigger_request) + end + + def create_next_builds + pipeline.create_next_builds(pipeline.builds.order(:id).last) + end + + it 'creates builds' do + expect(create_builds).to be_truthy + pipeline.builds.update_all(status: "success") + expect(pipeline.builds.count(:all)).to eq(2) + + expect(create_next_builds).to be_truthy + pipeline.builds.update_all(status: "success") + expect(pipeline.builds.count(:all)).to eq(4) + + expect(create_next_builds).to be_truthy + pipeline.builds.update_all(status: "success") + expect(pipeline.builds.count(:all)).to eq(5) + + expect(create_next_builds).to be_falsey + end + + context 'custom stage with first job allowed to fail' do + let(:yaml) do + { + stages: ['clean', 'test'], + clean_job: { + stage: 'clean', + allow_failure: true, + script: 'BUILD', + }, + test_job: { + stage: 'test', + script: 'TEST', + }, + } + end + + before do + stub_ci_pipeline_yaml_file(YAML.dump(yaml)) + create_builds + end + + it 'properly schedules builds' do + expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:drop) + expect(pipeline.builds.pluck(:status)).to contain_exactly('pending', 'failed') + end + end + + context 'properly creates builds when "when" is defined' do + let(:yaml) do + { + stages: ["build", "test", "test_failure", "deploy", "cleanup"], + build: { + stage: "build", + script: "BUILD", + }, + test: { + stage: "test", + script: "TEST", + }, + test_failure: { + stage: "test_failure", + script: "ON test failure", + when: "on_failure", + }, + deploy: { + stage: "deploy", + script: "PUBLISH", + }, + cleanup: { + stage: "cleanup", + script: "TIDY UP", + when: "always", + } + } + end + + before do + stub_ci_pipeline_yaml_file(YAML.dump(yaml)) + end + + context 'when builds are successful' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(pipeline.builds.pluck(:name)).to contain_exactly('build') + expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success') + pipeline.reload + expect(pipeline.status).to eq('success') + end + end + + context 'when test job fails' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(pipeline.builds.pluck(:name)).to contain_exactly('build') + expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:drop) + + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success') + pipeline.reload + expect(pipeline.status).to eq('failed') + end + end + + context 'when test and test_failure jobs fail' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(pipeline.builds.pluck(:name)).to contain_exactly('build') + expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:drop) + + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:drop) + + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success') + pipeline.reload + expect(pipeline.status).to eq('failed') + end + end + + context 'when deploy job fails' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(pipeline.builds.pluck(:name)).to contain_exactly('build') + expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') + pipeline.builds.running_or_pending.each(&:drop) + + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success') + pipeline.reload + expect(pipeline.status).to eq('failed') + end + end + + context 'when build is canceled in the second stage' do + it 'does not schedule builds after build has been canceled' do + expect(create_builds).to be_truthy + expect(pipeline.builds.pluck(:name)).to contain_exactly('build') + expect(pipeline.builds.pluck(:status)).to contain_exactly('pending') + pipeline.builds.running_or_pending.each(&:success) + + expect(pipeline.builds.running_or_pending).not_to be_empty + + expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending') + pipeline.builds.running_or_pending.each(&:cancel) + + expect(pipeline.builds.running_or_pending).to be_empty + expect(pipeline.reload.status).to eq('canceled') + end + end + end + end + + describe "#finished_at" do + let(:pipeline) { FactoryGirl.create :ci_pipeline } + + it "returns finished_at of latest build" do + build = FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 60 + FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 120 + + expect(pipeline.finished_at.to_i).to eq(build.finished_at.to_i) + end + + it "returns nil if there is no finished build" do + FactoryGirl.create :ci_not_started_build, pipeline: pipeline + + expect(pipeline.finished_at).to be_nil + end + end + + describe "coverage" do + let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" } + let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } + + it "calculates average when there are two builds with coverage" do + FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline + FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline + expect(pipeline.coverage).to eq("35.00") + end + + it "calculates average when there are two builds with coverage and one with nil" do + FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline + FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline + FactoryGirl.create :ci_build, pipeline: pipeline + expect(pipeline.coverage).to eq("35.00") + end + + it "calculates average when there are two builds with coverage and one is retried" do + FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline + FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, pipeline: pipeline + FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline + expect(pipeline.coverage).to eq("35.00") + end + + it "calculates average when there is one build without coverage" do + FactoryGirl.create :ci_build, pipeline: pipeline + expect(pipeline.coverage).to be_nil + end + end + + describe '#retryable?' do + subject { pipeline.retryable? } + + context 'no failed builds' do + before do + FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'success' + end + + it 'be not retryable' do + is_expected.to be_falsey + end + end + + context 'with failed builds' do + before do + FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'running' + FactoryGirl.create :ci_build, name: "rubocop", pipeline: pipeline, status: 'failed' + end + + it 'be retryable' do + is_expected.to be_truthy + end + end + end + + describe '#stages' do + let(:pipeline2) { FactoryGirl.create :ci_pipeline, project: project } + subject { CommitStatus.where(pipeline: [pipeline, pipeline2]).stages } + + before do + FactoryGirl.create :ci_build, pipeline: pipeline2, stage: 'test', stage_idx: 1 + FactoryGirl.create :ci_build, pipeline: pipeline, stage: 'build', stage_idx: 0 + end + + it 'return all stages' do + is_expected.to eq(%w(build test)) + end + end + + describe '#update_state' do + it 'execute update_state after touching object' do + expect(pipeline).to receive(:update_state).and_return(true) + pipeline.touch + end + + context 'dependent objects' do + let(:commit_status) { build :commit_status, pipeline: pipeline } + + it 'execute update_state after saving dependent object' do + expect(pipeline).to receive(:update_state).and_return(true) + commit_status.save + end + end + + context 'update state' do + let(:current) { Time.now.change(usec: 0) } + let(:build) { FactoryGirl.create :ci_build, :success, pipeline: pipeline, started_at: current - 120, finished_at: current - 60 } + + before do + build + end + + [:status, :started_at, :finished_at, :duration].each do |param| + it "update #{param}" do + expect(pipeline.send(param)).to eq(build.send(param)) + end + end + end + end + + describe '#branch?' do + subject { pipeline.branch? } + + context 'is not a tag' do + before do + pipeline.tag = false + end + + it 'return true when tag is set to false' do + is_expected.to be_truthy + end + end + + context 'is not a tag' do + before do + pipeline.tag = true + end + + it 'return false when tag is set to true' do + is_expected.to be_falsey + end + end + end +end diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb deleted file mode 100644 index 000a732db77..00000000000 --- a/spec/models/ci/runner_project_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -# == Schema Information -# -# Table name: ci_runner_projects -# -# id :integer not null, primary key -# runner_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# gl_project_id :integer -# - -require 'spec_helper' - -describe Ci::RunnerProject, models: true do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 25e9e5eca48..5d04d8ffcff 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -1,25 +1,24 @@ -# == Schema Information -# -# Table name: ci_runners -# -# id :integer not null, primary key -# token :string(255) -# created_at :datetime -# updated_at :datetime -# description :string(255) -# contacted_at :datetime -# active :boolean default(TRUE), not null -# is_shared :boolean default(FALSE) -# name :string(255) -# version :string(255) -# revision :string(255) -# platform :string(255) -# architecture :string(255) -# - require 'spec_helper' describe Ci::Runner, models: true do + describe 'validation' do + context 'when runner is not allowed to pick untagged jobs' do + context 'when runner does not have tags' do + it 'is not valid' do + runner = build(:ci_runner, tag_list: [], run_untagged: false) + expect(runner).to be_invalid + end + end + + context 'when runner has tags' do + it 'is valid' do + runner = build(:ci_runner, tag_list: ['tag'], run_untagged: false) + expect(runner).to be_valid + end + end + end + end + describe '#display_name' do it 'should return the description if it has a value' do runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448') @@ -133,7 +132,19 @@ describe Ci::Runner, models: true do end end - describe '#search' do + describe '#has_tags?' do + context 'when runner has tags' do + subject { create(:ci_runner, tag_list: ['tag']) } + it { is_expected.to have_tags } + end + + context 'when runner does not have tags' do + subject { create(:ci_runner, tag_list: []) } + it { is_expected.not_to have_tags } + end + end + + describe '.search' do let(:runner) { create(:ci_runner, token: '123abc') } it 'returns runners with a matching token' do diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb index 159be939300..474b0b1621d 100644 --- a/spec/models/ci/trigger_spec.rb +++ b/spec/models/ci/trigger_spec.rb @@ -1,16 +1,3 @@ -# == Schema Information -# -# Table name: ci_triggers -# -# id :integer not null, primary key -# token :string(255) -# project_id :integer -# deleted_at :datetime -# created_at :datetime -# updated_at :datetime -# gl_project_id :integer -# - require 'spec_helper' describe Ci::Trigger, models: true do diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 71e84091cb7..98f60087cf5 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -1,17 +1,3 @@ -# == Schema Information -# -# Table name: ci_variables -# -# id :integer not null, primary key -# project_id :integer -# key :string(255) -# value :text -# encrypted_value :text -# encrypted_value_salt :string(255) -# encrypted_value_iv :string(255) -# gl_project_id :integer -# - require 'spec_helper' describe Ci::Variable, models: true do @@ -37,7 +23,7 @@ describe Ci::Variable, models: true do end it 'fails to decrypt if iv is incorrect' do - subject.encrypted_value_iv = nil + subject.encrypted_value_iv = SecureRandom.hex subject.instance_variable_set(:@value, nil) expect { subject.value }. to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt') diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb index 9307d97e214..384a38ebc69 100644 --- a/spec/models/commit_range_spec.rb +++ b/spec/models/commit_range_spec.rb @@ -24,6 +24,16 @@ describe CommitRange, models: true do expect { described_class.new("Foo", project) }.to raise_error(ArgumentError) end + describe '#initialize' do + it 'does not modify strings in-place' do + input = "#{sha_from}...#{sha_to} " + + described_class.new(input, project) + + expect(input).to eq("#{sha_from}...#{sha_to} ") + end + end + describe '#to_s' do it 'is correct for three-dot syntax' do expect(range.to_s).to eq "#{full_sha_from}...#{full_sha_to}" @@ -135,4 +145,28 @@ describe CommitRange, models: true do end end end + + describe '#has_been_reverted?' do + it 'returns true if the commit has been reverted' do + issue = create(:issue) + + create(:note_on_issue, + noteable: issue, + system: true, + note: commit1.revert_description, + project: issue.project) + + expect_any_instance_of(Commit).to receive(:reverts_commit?). + with(commit1). + and_return(true) + + expect(commit1.has_been_reverted?(nil, issue)).to eq(true) + end + + it 'returns false a commit has not been reverted' do + issue = create(:issue) + + expect(commit1.has_been_reverted?(nil, issue)).to eq(false) + end + end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index ad47e338a33..beca8708c9d 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Commit, models: true do - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:commit) { project.commit } describe 'modules' do @@ -56,7 +56,7 @@ describe Commit, models: true do end it "does not truncates a message with a newline after 80 but less 100 characters" do - message =<<eos + message = <<eos Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris. eos @@ -171,4 +171,40 @@ eos describe '#status' do # TODO: kamil end + + describe '#participants' do + let(:user1) { build(:user) } + let(:user2) { build(:user) } + + let!(:note1) do + create(:note_on_commit, + commit_id: commit.id, + project: project, + note: 'foo') + end + + let!(:note2) do + create(:note_on_commit, + commit_id: commit.id, + project: project, + note: 'bar') + end + + before do + allow(commit).to receive(:author).and_return(user1) + allow(commit).to receive(:committer).and_return(user2) + end + + it 'includes the commit author' do + expect(commit.participants).to include(commit.author) + end + + it 'includes the committer' do + expect(commit.participants).to include(commit.committer) + end + + it 'includes the authors of the commit notes' do + expect(commit.participants).to include(note1.author, note2.author) + end + end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 971e6750375..8fb605fff8a 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -1,52 +1,18 @@ -# == Schema Information -# -# Table name: ci_builds -# -# id :integer not null, primary key -# project_id :integer -# status :string(255) -# finished_at :datetime -# trace :text -# created_at :datetime -# updated_at :datetime -# started_at :datetime -# runner_id :integer -# coverage :float -# commit_id :integer -# commands :text -# job_id :integer -# name :string(255) -# deploy :boolean default(FALSE) -# options :text -# allow_failure :boolean default(FALSE), not null -# stage :string(255) -# trigger_request_id :integer -# stage_idx :integer -# tag :boolean -# ref :string(255) -# user_id :integer -# type :string(255) -# target_url :string(255) -# description :string(255) -# artifacts_file :text -# gl_project_id :integer -# - require 'spec_helper' describe CommitStatus, models: true do - let(:commit) { FactoryGirl.create :ci_commit } - let(:commit_status) { FactoryGirl.create :commit_status, commit: commit } + let(:pipeline) { FactoryGirl.create :ci_pipeline } + let(:commit_status) { FactoryGirl.create :commit_status, pipeline: pipeline } - it { is_expected.to belong_to(:commit) } + it { is_expected.to belong_to(:pipeline) } it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:project) } it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_inclusion_of(:status).in_array(%w(pending running failed success canceled)) } - it { is_expected.to delegate_method(:sha).to(:commit) } - it { is_expected.to delegate_method(:short_sha).to(:commit) } + it { is_expected.to delegate_method(:sha).to(:pipeline) } + it { is_expected.to delegate_method(:short_sha).to(:pipeline) } it { is_expected.to respond_to :success? } it { is_expected.to respond_to :failed? } @@ -155,11 +121,11 @@ describe CommitStatus, models: true do subject { CommitStatus.latest.order(:id) } before do - @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running' - @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending' - @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'cc', status: 'success' - @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'bb', status: 'success' - @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'success' + @commit1 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'running' + @commit2 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'cc', status: 'pending' + @commit3 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'cc', status: 'success' + @commit4 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'bb', status: 'success' + @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'success' end it 'return unique statuses' do @@ -171,11 +137,11 @@ describe CommitStatus, models: true do subject { CommitStatus.running_or_pending.order(:id) } before do - @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running' - @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending' - @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success' - @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'dd', ref: nil, status: 'failed' - @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'ee', ref: nil, status: 'canceled' + @commit1 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'running' + @commit2 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'cc', status: 'pending' + @commit3 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: nil, status: 'success' + @commit4 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'dd', ref: nil, status: 'failed' + @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'ee', ref: nil, status: 'canceled' end it 'return statuses that are running or pending' do @@ -186,17 +152,17 @@ describe CommitStatus, models: true do describe '#before_sha' do subject { commit_status.before_sha } - context 'when no before_sha is set for ci::commit' do - before { commit.before_sha = nil } + context 'when no before_sha is set for pipeline' do + before { pipeline.before_sha = nil } it 'return blank sha' do is_expected.to eq(Gitlab::Git::BLANK_SHA) end end - context 'for before_sha set for ci::commit' do + context 'for before_sha set for pipeline' do let(:value) { '1234' } - before { commit.before_sha = value } + before { pipeline.before_sha = value } it 'return the set value' do is_expected.to eq(value) @@ -206,14 +172,14 @@ describe CommitStatus, models: true do describe '#stages' do before do - FactoryGirl.create :commit_status, commit: commit, stage: 'build', stage_idx: 0, status: 'success' - FactoryGirl.create :commit_status, commit: commit, stage: 'build', stage_idx: 0, status: 'failed' - FactoryGirl.create :commit_status, commit: commit, stage: 'deploy', stage_idx: 2, status: 'running' - FactoryGirl.create :commit_status, commit: commit, stage: 'test', stage_idx: 1, status: 'success' + FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'build', stage_idx: 0, status: 'success' + FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'build', stage_idx: 0, status: 'failed' + FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'deploy', stage_idx: 2, status: 'running' + FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'test', stage_idx: 1, status: 'success' end context 'stages list' do - subject { CommitStatus.where(commit: commit).stages } + subject { CommitStatus.where(pipeline: pipeline).stages } it 'return ordered list of stages' do is_expected.to eq(%w(build test deploy)) @@ -221,7 +187,7 @@ describe CommitStatus, models: true do end context 'stages with statuses' do - subject { CommitStatus.where(commit: commit).stages_status } + subject { CommitStatus.where(pipeline: pipeline).stages_status } it 'return list of stages with statuses' do is_expected.to eq({ diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb new file mode 100644 index 00000000000..a371c4a18a9 --- /dev/null +++ b/spec/models/concerns/awardable_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Issue, "Awardable" do + let!(:issue) { create(:issue) } + let!(:award_emoji) { create(:award_emoji, :downvote, awardable: issue) } + + describe "Associations" do + it { is_expected.to have_many(:award_emoji).dependent(:destroy) } + end + + describe "ClassMethods" do + let!(:issue2) { create(:issue) } + + before do + create(:award_emoji, awardable: issue2) + end + + it "orders on upvotes" do + expect(Issue.order_upvotes_desc.to_a).to eq [issue2, issue] + end + + it "orders on downvotes" do + expect(Issue.order_downvotes_desc.to_a).to eq [issue, issue2] + end + end + + describe "#upvotes" do + it "counts the number of upvotes" do + expect(issue.upvotes).to be 0 + end + end + + describe "#downvotes" do + it "counts the number of downvotes" do + expect(issue.downvotes).to be 1 + end + end + + describe "#toggle_award_emoji" do + it "adds an emoji if it isn't awarded yet" do + expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by(1) + end + + it "toggles already awarded emoji" do + expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by(-1) + end + end +end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 4a4cd093435..efbcbf72f76 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -10,6 +10,20 @@ describe Issue, "Issuable" do it { is_expected.to belong_to(:assignee) } it { is_expected.to have_many(:notes).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) } + + context 'Notes' do + let!(:note) { create(:note, noteable: issue, project: issue.project) } + let(:scoped_issue) { Issue.includes(notes: :author).find(issue.id) } + + it 'indicates if the notes have their authors loaded' do + expect(issue.notes).not_to be_authors_loaded + expect(scoped_issue.notes).to be_authors_loaded + end + end + end + + describe 'Included modules' do + it { is_expected.to include_module(Awardable) } end describe "Validation" do @@ -114,6 +128,35 @@ describe Issue, "Issuable" do end end + describe "#sort" do + let(:project) { build_stubbed(:empty_project) } + + context "by milestone due date" do + # Correct order is: + # Issues/MRs with milestones ordered by date + # Issues/MRs with milestones without dates + # Issues/MRs without milestones + + let!(:issue) { create(:issue, project: project) } + let!(:early_milestone) { create(:milestone, project: project, due_date: 10.days.from_now) } + let!(:late_milestone) { create(:milestone, project: project, due_date: 30.days.from_now) } + let!(:issue1) { create(:issue, project: project, milestone: early_milestone) } + let!(:issue2) { create(:issue, project: project, milestone: late_milestone) } + let!(:issue3) { create(:issue, project: project) } + + it "sorts desc" do + issues = project.issues.sort('milestone_due_desc') + expect(issues).to match_array([issue2, issue1, issue, issue3]) + end + + it "sorts asc" do + issues = project.issues.sort('milestone_due_asc') + expect(issues).to match_array([issue1, issue2, issue, issue3]) + end + end + end + + describe '#subscribed?' do context 'user is not a participant in the issue' do before { allow(issue).to receive(:participants).with(user).and_return([]) } @@ -160,12 +203,11 @@ describe Issue, "Issuable" do let(:data) { issue.to_hook_data(user) } let(:project) { issue.project } - it "returns correct hook data" do expect(data[:object_kind]).to eq("issue") expect(data[:user]).to eq(user.hook_attrs) expect(data[:object_attributes]).to eq(issue.hook_attrs) - expect(data).to_not have_key(:assignee) + expect(data).not_to have_key(:assignee) end context "issue is assigned" do @@ -199,12 +241,42 @@ describe Issue, "Issuable" do end end + describe '#labels_array' do + let(:project) { create(:project) } + let(:bug) { create(:label, project: project, title: 'bug') } + let(:issue) { create(:issue, project: project) } + + before(:each) do + issue.labels << bug + end + + it 'loads the association and returns it as an array' do + expect(issue.reload.labels_array).to eq([bug]) + end + end + + describe '#user_notes_count' do + let(:project) { create(:project) } + let(:issue1) { create(:issue, project: project) } + let(:issue2) { create(:issue, project: project) } + + before do + create_list(:note, 3, noteable: issue1, project: project) + create_list(:note, 6, noteable: issue2, project: project) + end + + it 'counts the user notes' do + expect(issue1.user_notes_count).to be(3) + expect(issue2.user_notes_count).to be(6) + end + end + describe "votes" do + let(:project) { issue.project } + before do - author = create :user - project = create :empty_project - issue.notes.awards.create!(note: "thumbsup", author: author, project: project) - issue.notes.awards.create!(note: "thumbsdown", author: author, project: project) + create(:award_emoji, :upvote, awardable: issue) + create(:award_emoji, :downvote, awardable: issue) end it "returns correct values" do diff --git a/spec/models/concerns/participable_spec.rb b/spec/models/concerns/participable_spec.rb new file mode 100644 index 00000000000..7e4ea0f2d66 --- /dev/null +++ b/spec/models/concerns/participable_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe Participable, models: true do + let(:model) do + Class.new do + include Participable + end + end + + describe '.participant' do + it 'adds the participant attributes to the existing list' do + model.participant(:foo) + model.participant(:bar) + + expect(model.participant_attrs).to eq([:foo, :bar]) + end + end + + describe '#participants' do + it 'returns the list of participants' do + model.participant(:foo) + model.participant(:bar) + + user1 = build(:user) + user2 = build(:user) + user3 = build(:user) + project = build(:project, :public) + instance = model.new + + expect(instance).to receive(:foo).and_return(user2) + expect(instance).to receive(:bar).and_return(user3) + expect(instance).to receive(:project).twice.and_return(project) + + participants = instance.participants(user1) + + expect(participants).to include(user2) + expect(participants).to include(user3) + end + + it 'supports attributes returning another Participable' do + other_model = Class.new { include Participable } + + other_model.participant(:bar) + model.participant(:foo) + + instance = model.new + other = other_model.new + user1 = build(:user) + user2 = build(:user) + project = build(:project, :public) + + expect(instance).to receive(:foo).and_return(other) + expect(other).to receive(:bar).and_return(user2) + expect(instance).to receive(:project).twice.and_return(project) + + expect(instance.participants(user1)).to eq([user2]) + end + + context 'when using a Proc as an attribute' do + it 'calls the supplied Proc' do + user1 = build(:user) + project = build(:project, :public) + + user_arg = nil + ext_arg = nil + + model.participant -> (user, ext) do + user_arg = user + ext_arg = ext + end + + instance = model.new + + expect(instance).to receive(:project).twice.and_return(project) + + instance.participants(user1) + + expect(user_arg).to eq(user1) + expect(ext_arg).to be_an_instance_of(Gitlab::ReferenceExtractor) + end + end + end +end diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb index e31fdb0bffb..b7fc5a92497 100644 --- a/spec/models/concerns/subscribable_spec.rb +++ b/spec/models/concerns/subscribable_spec.rb @@ -44,6 +44,16 @@ describe Subscribable, 'Subscribable' do end end + describe '#subscribe' do + it 'subscribes the given user' do + expect(resource.subscribed?(user)).to be_falsey + + resource.subscribe(user) + + expect(resource.subscribed?(user)).to be_truthy + end + end + describe '#unsubscribe' do it 'unsubscribes the given current user' do resource.subscriptions.create(user: user, subscribed: true) diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index 30c0a04b840..9e8ebc56a31 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -28,14 +28,14 @@ describe ApplicationSetting, 'TokenAuthenticatable' do context 'token is not generated yet' do describe 'token field accessor' do subject { described_class.new.send(token_field) } - it { is_expected.to_not be_blank } + it { is_expected.not_to be_blank } end describe 'ensured token' do subject { described_class.new.send("ensure_#{token_field}") } it { is_expected.to be_a String } - it { is_expected.to_not be_blank } + it { is_expected.not_to be_blank } end describe 'ensured! token' do @@ -49,7 +49,7 @@ describe ApplicationSetting, 'TokenAuthenticatable' do context 'token is generated' do before { subject.send("reset_#{token_field}!") } - it 'persists a new token 'do + it 'persists a new token' do expect(subject.send(:read_attribute, token_field)).to be_a String end end diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb index 64ba778afea..6a90598a629 100644 --- a/spec/models/deploy_key_spec.rb +++ b/spec/models/deploy_key_spec.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: keys -# -# id :integer not null, primary key -# user_id :integer -# created_at :datetime -# updated_at :datetime -# key :text -# title :string(255) -# type :string(255) -# fingerprint :string(255) -# public :boolean default(FALSE), not null -# - require 'spec_helper' describe DeployKey, models: true do diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb index 8aedbfb8636..8a1e337c1a3 100644 --- a/spec/models/deploy_keys_project_spec.rb +++ b/spec/models/deploy_keys_project_spec.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: deploy_keys_projects -# -# id :integer not null, primary key -# deploy_key_id :integer not null -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# - require 'spec_helper' describe DeployKeysProject, models: true do diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb index a20a6149649..5d0bd31db5a 100644 --- a/spec/models/email_spec.rb +++ b/spec/models/email_spec.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: emails -# -# id :integer not null, primary key -# user_id :integer not null -# email :string(255) not null -# created_at :datetime -# updated_at :datetime -# - require 'spec_helper' describe Email, models: true do diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 0c3cd13f399..b0e76fec693 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -1,19 +1,3 @@ -# == Schema Information -# -# Table name: events -# -# id :integer not null, primary key -# target_type :string(255) -# target_id :integer -# title :string(255) -# data :text -# project_id :integer -# created_at :datetime -# updated_at :datetime -# action :integer -# author_id :integer -# - require 'spec_helper' describe Event, models: true do diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb index d90fbfe1ea5..3b817608ce0 100644 --- a/spec/models/forked_project_link_spec.rb +++ b/spec/models/forked_project_link_spec.rb @@ -1,14 +1,3 @@ -# == Schema Information -# -# Table name: forked_project_links -# -# id :integer not null, primary key -# forked_to_project_id :integer not null -# forked_from_project_id :integer not null -# created_at :datetime -# updated_at :datetime -# - require 'spec_helper' describe ForkedProjectLink, "add link on fork" do diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index 5b0883d8702..c4e781dd1dc 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -1,42 +1,8 @@ -# == Schema Information -# -# Table name: ci_builds -# -# id :integer not null, primary key -# project_id :integer -# status :string(255) -# finished_at :datetime -# trace :text -# created_at :datetime -# updated_at :datetime -# started_at :datetime -# runner_id :integer -# coverage :float -# commit_id :integer -# commands :text -# job_id :integer -# name :string(255) -# deploy :boolean default(FALSE) -# options :text -# allow_failure :boolean default(FALSE), not null -# stage :string(255) -# trigger_request_id :integer -# stage_idx :integer -# tag :boolean -# ref :string(255) -# user_id :integer -# type :string(255) -# target_url :string(255) -# description :string(255) -# artifacts_file :text -# gl_project_id :integer -# - require 'spec_helper' describe GenericCommitStatus, models: true do - let(:commit) { FactoryGirl.create :ci_commit } - let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, commit: commit } + let(:pipeline) { FactoryGirl.create :ci_pipeline } + let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline } describe :context do subject { generic_commit_status.context } @@ -61,13 +27,13 @@ describe GenericCommitStatus, models: true do describe :context do subject { generic_commit_status.context } - it { is_expected.to_not be_nil } + it { is_expected.not_to be_nil } end describe :stage do subject { generic_commit_status.stage } - it { is_expected.to_not be_nil } + it { is_expected.not_to be_nil } end end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 7bfca1e72c3..6fa16be7f04 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: namespaces -# -# id :integer not null, primary key -# name :string(255) not null -# path :string(255) not null -# owner_id :integer -# created_at :datetime -# updated_at :datetime -# type :string(255) -# description :string(255) default(""), not null -# avatar :string(255) -# - require 'spec_helper' describe Group, models: true do diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb index f800f415bd2..534e1b4f128 100644 --- a/spec/models/hooks/service_hook_spec.rb +++ b/spec/models/hooks/service_hook_spec.rb @@ -34,14 +34,14 @@ describe ServiceHook, models: true do it "POSTs to the webhook URL" do @service_hook.execute(@data) expect(WebMock).to have_requested(:post, @service_hook.url).with( - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Service Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Service Hook' } ).once end it "POSTs the data as JSON" do @service_hook.execute(@data) expect(WebMock).to have_requested(:post, @service_hook.url).with( - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Service Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Service Hook' } ).once end diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 56a9fbe9720..4078b9e4ff5 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -33,7 +33,7 @@ describe SystemHook, models: true do Projects::CreateService.new(user, name: 'empty').execute expect(WebMock).to have_requested(:post, system_hook.url).with( body: /project_create/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -42,7 +42,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /project_destroy/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -51,7 +51,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_create/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -60,7 +60,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_destroy/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -69,7 +69,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_add_to_team/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -79,7 +79,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_remove_from_team/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -88,7 +88,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /group_create/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -97,7 +97,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /group_destroy/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -106,7 +106,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_add_to_group/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end @@ -116,7 +116,7 @@ describe SystemHook, models: true do expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_remove_from_group/, - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'System Hook' } ).once end end diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 37a27d73aab..f9bab487b96 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -95,13 +95,13 @@ describe WebHook, models: true do it "handles 200 status code" do WebMock.stub_request(:post, project_hook.url).to_return(status: 200, body: "Success") - expect(project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success']) + expect(project_hook.execute(@data, 'push_hooks')).to eq([200, 'Success']) end it "handles 2xx status codes" do WebMock.stub_request(:post, project_hook.url).to_return(status: 201, body: "Success") - expect(project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success']) + expect(project_hook.execute(@data, 'push_hooks')).to eq([201, 'Success']) end end end diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb index 5afe042e154..1b987588f59 100644 --- a/spec/models/identity_spec.rb +++ b/spec/models/identity_spec.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: identities -# -# id :integer not null, primary key -# extern_uid :string(255) -# provider :string(255) -# user_id :integer -# created_at :datetime -# updated_at :datetime -# - require 'spec_helper' RSpec.describe Identity, models: true do diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 060e6599104..b87d68283e6 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -1,23 +1,3 @@ -# == Schema Information -# -# Table name: issues -# -# id :integer not null, primary key -# title :string(255) -# assignee_id :integer -# author_id :integer -# project_id :integer -# created_at :datetime -# updated_at :datetime -# position :integer default(0) -# branch_name :string(255) -# description :text -# milestone_id :integer -# state :string(255) -# iid :integer -# updated_by_id :integer -# - require 'spec_helper' describe Issue, models: true do @@ -212,7 +192,7 @@ describe Issue, models: true do source_project: subject.project, source_branch: "#{subject.iid}-branch" }) merge_request.create_cross_references!(user) - expect(subject.referenced_merge_requests).to_not be_empty + expect(subject.referenced_merge_requests).not_to be_empty expect(subject.related_branches(user)).to eq([subject.to_branch_name]) end @@ -251,4 +231,59 @@ describe Issue, models: true do expect(issue.to_branch_name).to match /confidential-issue\z/ end end + + describe '#participants' do + context 'using a public project' do + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + + let!(:note1) do + create(:note_on_issue, noteable: issue, project: project, note: 'a') + end + + let!(:note2) do + create(:note_on_issue, noteable: issue, project: project, note: 'b') + end + + it 'includes the issue author' do + expect(issue.participants).to include(issue.author) + end + + it 'includes the authors of the notes' do + expect(issue.participants).to include(note1.author, note2.author) + end + end + + context 'using a private project' do + it 'does not include mentioned users that do not have access to the project' do + project = create(:project) + user = create(:user) + issue = create(:issue, project: project) + + create(:note_on_issue, + noteable: issue, + project: project, + note: user.to_reference) + + expect(issue.participants).not_to include(user) + end + end + end + + describe 'cached counts' do + it 'updates when assignees change' do + user1 = create(:user) + user2 = create(:user) + issue = create(:issue, assignee: user1) + + expect(user1.assigned_open_issues_count).to eq(1) + expect(user2.assigned_open_issues_count).to eq(0) + + issue.assignee = user2 + issue.save + + expect(user1.assigned_open_issues_count).to eq(0) + expect(user2.assigned_open_issues_count).to eq(1) + end + end end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index c962b83644a..26fbedbef2f 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: keys -# -# id :integer not null, primary key -# user_id :integer -# created_at :datetime -# updated_at :datetime -# key :text -# title :string(255) -# type :string(255) -# fingerprint :string(255) -# public :boolean default(FALSE), not null -# - require 'spec_helper' describe Key, models: true do diff --git a/spec/models/label_link_spec.rb b/spec/models/label_link_spec.rb index dc7510b1de3..5e6f8ca1528 100644 --- a/spec/models/label_link_spec.rb +++ b/spec/models/label_link_spec.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: label_links -# -# id :integer not null, primary key -# label_id :integer -# target_id :integer -# target_type :string(255) -# created_at :datetime -# updated_at :datetime -# - require 'spec_helper' describe LabelLink, models: true do diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index 0614ca1e7c9..dad2628651b 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -1,16 +1,3 @@ -# == Schema Information -# -# Table name: labels -# -# id :integer not null, primary key -# title :string(255) -# color :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# template :boolean default(FALSE) -# - require 'spec_helper' describe Label, models: true do @@ -55,6 +42,14 @@ describe Label, models: true do end end + describe "#title" do + let(:label) { create(:label, title: "<b>test</b>") } + + it "sanitizes title" do + expect(label.title).to eq("test") + end + end + describe '#to_reference' do context 'using id' do it 'returns a String reference to the object' do diff --git a/spec/models/legacy_diff_note_spec.rb b/spec/models/legacy_diff_note_spec.rb new file mode 100644 index 00000000000..b2d06853886 --- /dev/null +++ b/spec/models/legacy_diff_note_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +describe LegacyDiffNote, models: true do + describe "Commit diff line notes" do + let!(:note) { create(:note_on_commit_diff, note: "+1 from me") } + let!(:commit) { note.noteable } + + it "should save a valid note" do + expect(note.commit_id).to eq(commit.id) + expect(note.noteable.id).to eq(commit.id) + end + + it "should be recognized by #legacy_diff_note?" do + expect(note).to be_legacy_diff_note + end + end + + describe '#active?' do + it 'is always true when the note has no associated diff' do + note = build(:note_on_merge_request_diff) + + expect(note).to receive(:diff).and_return(nil) + + expect(note).to be_active + end + + it 'is never true when the note has no noteable associated' do + note = build(:note_on_merge_request_diff) + + expect(note).to receive(:diff).and_return(double) + expect(note).to receive(:noteable).and_return(nil) + + expect(note).not_to be_active + end + + it 'returns the memoized value if defined' do + note = build(:note_on_merge_request_diff) + + note.instance_variable_set(:@active, 'foo') + expect(note).not_to receive(:find_noteable_diff) + + expect(note.active?).to eq 'foo' + end + + context 'for a merge request noteable' do + it 'is false when noteable has no matching diff' do + merge = build_stubbed(:merge_request, :simple) + note = build(:note_on_merge_request_diff, noteable: merge) + + allow(note).to receive(:diff).and_return(double) + expect(note).to receive(:find_noteable_diff).and_return(nil) + + expect(note).not_to be_active + end + + it 'is true when noteable has a matching diff' do + merge = create(:merge_request, :simple) + + # Generate a real line_code value so we know it will match. We use a + # random line from a random diff just for funsies. + diff = merge.diffs.to_a.sample + line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample + code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos) + + # We're persisting in order to trigger the set_diff callback + note = create(:note_on_merge_request_diff, noteable: merge, + line_code: code, + project: merge.source_project) + + # Make sure we don't get a false positive from a guard clause + expect(note).to receive(:find_noteable_diff).and_call_original + expect(note).to be_active + end + end + end +end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 2d8f1cc1ad3..6e51730eecd 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -1,22 +1,3 @@ -# == Schema Information -# -# Table name: members -# -# id :integer not null, primary key -# access_level :integer not null -# source_id :integer not null -# source_type :string(255) not null -# user_id :integer -# notification_level :integer not null -# type :string(255) -# created_at :datetime -# updated_at :datetime -# created_by_id :integer -# invite_email :string(255) -# invite_token :string(255) -# invite_accepted_at :datetime -# - require 'spec_helper' describe Member, models: true do diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 9f26d9eb5ce..9f13874b532 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -20,6 +20,48 @@ require 'spec_helper' describe ProjectMember, models: true do + describe 'associations' do + it { is_expected.to belong_to(:project).class_name('Project').with_foreign_key(:source_id) } + end + + describe 'validations' do + it { is_expected.to allow_value('Project').for(:source_type) } + it { is_expected.not_to allow_value('project').for(:source_type) } + end + + describe 'modules' do + it { is_expected.to include_module(Gitlab::ShellAdapter) } + end + + describe "#destroy" do + let(:owner) { create(:project_member, access_level: ProjectMember::OWNER) } + let(:project) { owner.project } + let(:master) { create(:project_member, project: project) } + + let(:owner_todos) { (0...2).map { create(:todo, user: owner.user, project: project) } } + let(:master_todos) { (0...3).map { create(:todo, user: master.user, project: project) } } + + before do + owner_todos + master_todos + end + + it "destroy itself and delete associated todos" do + expect(owner.user.todos.size).to eq(2) + expect(master.user.todos.size).to eq(3) + expect(Todo.count).to eq(5) + + master_todo_ids = master_todos.map(&:id) + master.destroy + + expect(owner.user.todos.size).to eq(2) + expect(Todo.count).to eq(2) + master_todo_ids.each do |id| + expect(Todo.exists?(id)).to eq(false) + end + end + end + describe :import_team do before do @abilities = Six.new diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index d7884cea336..3b199f4d98d 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1,32 +1,3 @@ -# == Schema Information -# -# Table name: merge_requests -# -# id :integer not null, primary key -# target_branch :string(255) not null -# source_branch :string(255) not null -# source_project_id :integer not null -# author_id :integer -# assignee_id :integer -# title :string(255) -# created_at :datetime -# updated_at :datetime -# milestone_id :integer -# state :string(255) -# merge_status :string(255) -# target_project_id :integer not null -# iid :integer -# description :text -# position :integer default(0) -# locked_at :datetime -# updated_by_id :integer -# merge_error :string(255) -# merge_params :text -# merge_when_build_succeeds :boolean default(FALSE), not null -# merge_user_id :integer -# merge_commit_sha :string -# - require 'spec_helper' describe MergeRequest, models: true do @@ -93,7 +64,13 @@ describe MergeRequest, models: true do describe '#target_sha' do context 'when the target branch does not exist anymore' do - subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } } + let(:project) { create(:project) } + + subject { create(:merge_request, source_project: project, target_project: project) } + + before do + project.repository.raw_repository.delete_branch(subject.target_branch) + end it 'returns nil' do expect(subject.target_sha).to be_nil @@ -142,7 +119,8 @@ describe MergeRequest, models: true do before do allow(merge_request).to receive(:commits) { [merge_request.source_project.repository.commit] } - create(:note, commit_id: merge_request.commits.first.id, noteable_type: 'Commit', project: merge_request.project) + create(:note_on_commit, commit_id: merge_request.commits.first.id, + project: merge_request.project) create(:note, noteable: merge_request, project: merge_request.project) end @@ -152,7 +130,9 @@ describe MergeRequest, models: true do end it "should include notes for commits from target project as well" do - create(:note, commit_id: merge_request.commits.first.id, noteable_type: 'Commit', project: merge_request.target_project) + create(:note_on_commit, commit_id: merge_request.commits.first.id, + project: merge_request.target_project) + expect(merge_request.commits).not_to be_empty expect(merge_request.mr_and_commit_notes.count).to eq(3) end @@ -283,13 +263,18 @@ describe MergeRequest, models: true do end describe "#reset_merge_when_build_succeeds" do - let(:merge_if_green) { create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user) } + let(:merge_if_green) do + create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user), + merge_params: { "should_remove_source_branch" => "1", "commit_message" => "msg" } + end it "sets the item to false" do merge_if_green.reset_merge_when_build_succeeds merge_if_green.reload expect(merge_if_green.merge_when_build_succeeds).to be_falsey + expect(merge_if_green.merge_params["should_remove_source_branch"]).to be_nil + expect(merge_if_green.merge_params["commit_message"]).to be_nil end end @@ -318,7 +303,12 @@ describe MergeRequest, models: true do let(:fork_project) { create(:project, forked_from_project: project) } context 'when the target branch does not exist anymore' do - subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } } + subject { create(:merge_request, source_project: project, target_project: project) } + + before do + project.repository.raw_repository.delete_branch(subject.target_branch) + subject.reload + end it 'does not crash' do expect{ subject.diverged_commits_count }.not_to raise_error @@ -400,19 +390,19 @@ describe MergeRequest, models: true do subject { create :merge_request, :simple } end - describe '#ci_commit' do + describe '#pipeline' do describe 'when the source project exists' do it 'returns the latest commit' do - commit = double(:commit, id: '123abc') - ci_commit = double(:ci_commit, ref: 'master') + commit = double(:commit, id: '123abc') + pipeline = double(:ci_pipeline, ref: 'master') allow(subject).to receive(:last_commit).and_return(commit) - expect(subject.source_project).to receive(:ci_commit). + expect(subject.source_project).to receive(:pipeline). with('123abc', 'master'). - and_return(ci_commit) + and_return(pipeline) - expect(subject.ci_commit).to eq(ci_commit) + expect(subject.pipeline).to eq(pipeline) end end @@ -420,7 +410,201 @@ describe MergeRequest, models: true do it 'returns nil' do allow(subject).to receive(:source_project).and_return(nil) - expect(subject.ci_commit).to be_nil + expect(subject.pipeline).to be_nil + end + end + end + + describe '#participants' do + let(:project) { create(:project, :public) } + + let(:mr) do + create(:merge_request, source_project: project, target_project: project) + end + + let!(:note1) do + create(:note_on_merge_request, noteable: mr, project: project, note: 'a') + end + + let!(:note2) do + create(:note_on_merge_request, noteable: mr, project: project, note: 'b') + end + + it 'includes the merge request author' do + expect(mr.participants).to include(mr.author) + end + + it 'includes the authors of the notes' do + expect(mr.participants).to include(note1.author, note2.author) + end + end + + describe 'cached counts' do + it 'updates when assignees change' do + user1 = create(:user) + user2 = create(:user) + mr = create(:merge_request, assignee: user1) + + expect(user1.assigned_open_merge_request_count).to eq(1) + expect(user2.assigned_open_merge_request_count).to eq(0) + + mr.assignee = user2 + mr.save + + expect(user1.assigned_open_merge_request_count).to eq(0) + expect(user2.assigned_open_merge_request_count).to eq(1) + end + end + + describe '#check_if_can_be_merged' do + let(:project) { create(:project, only_allow_merge_if_build_succeeds: true) } + + subject { create(:merge_request, source_project: project, merge_status: :unchecked) } + + context 'when it is not broken and has no conflicts' do + it 'is marked as mergeable' do + allow(subject).to receive(:broken?) { false } + allow(project).to receive_message_chain(:repository, :can_be_merged?) { true } + + expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged') + end + end + + context 'when broken' do + before { allow(subject).to receive(:broken?) { true } } + + it 'becomes unmergeable' do + expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') + end + end + + context 'when it has conflicts' do + before do + allow(subject).to receive(:broken?) { false } + allow(project).to receive_message_chain(:repository, :can_be_merged?) { false } + end + + it 'becomes unmergeable' do + expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') + end + end + end + + describe '#mergeable?' do + let(:project) { create(:project) } + + subject { create(:merge_request, source_project: project) } + + it 'returns false if #mergeable_state? is false' do + expect(subject).to receive(:mergeable_state?) { false } + + expect(subject.mergeable?).to be_falsey + end + + it 'return true if #mergeable_state? is true and the MR #can_be_merged? is true' do + allow(subject).to receive(:mergeable_state?) { true } + expect(subject).to receive(:check_if_can_be_merged) + expect(subject).to receive(:can_be_merged?) { true } + + expect(subject.mergeable?).to be_truthy + end + end + + describe '#mergeable_state?' do + let(:project) { create(:project) } + + subject { create(:merge_request, source_project: project) } + + it 'checks if merge request can be merged' do + allow(subject).to receive(:mergeable_ci_state?) { true } + expect(subject).to receive(:check_if_can_be_merged) + + subject.mergeable? + end + + context 'when not open' do + before { subject.close } + + it 'returns false' do + expect(subject.mergeable_state?).to be_falsey + end + end + + context 'when working in progress' do + before { subject.title = 'WIP MR' } + + it 'returns false' do + expect(subject.mergeable_state?).to be_falsey + end + end + + context 'when broken' do + before { allow(subject).to receive(:broken?) { true } } + + it 'returns false' do + expect(subject.mergeable_state?).to be_falsey + end + end + + context 'when failed' do + before { allow(subject).to receive(:broken?) { false } } + + context 'when project settings restrict to merge only if build succeeds and build failed' do + before do + project.only_allow_merge_if_build_succeeds = true + allow(subject).to receive(:mergeable_ci_state?) { false } + end + + it 'returns false' do + expect(subject.mergeable_state?).to be_falsey + end + end + end + end + + describe '#mergeable_ci_state?' do + let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) } + let(:pipeline) { create(:ci_empty_pipeline) } + + subject { build(:merge_request, target_project: project) } + + context 'when it is only allowed to merge when build is green' do + context 'and a failed pipeline is associated' do + before do + pipeline.statuses << create(:commit_status, status: 'failed', project: project) + allow(subject).to receive(:pipeline) { pipeline } + end + + it { expect(subject.mergeable_ci_state?).to be_falsey } + end + + context 'when no pipeline is associated' do + before do + allow(subject).to receive(:pipeline) { nil } + end + + it { expect(subject.mergeable_ci_state?).to be_truthy } + end + end + + context 'when merges are not restricted to green builds' do + subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_build_succeeds: false)) } + + context 'and a failed pipeline is associated' do + before do + pipeline.statuses << create(:commit_status, status: 'failed', project: project) + allow(subject).to receive(:pipeline) { pipeline } + end + + it { expect(subject.mergeable_ci_state?).to be_truthy } + end + + context 'when no pipeline is associated' do + before do + allow(subject).to receive(:pipeline) { nil } + end + + it { expect(subject.mergeable_ci_state?).to be_truthy } end end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 72a4ea70228..1e18c788b50 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: milestones -# -# id :integer not null, primary key -# title :string(255) not null -# project_id :integer not null -# description :text -# due_date :date -# created_at :datetime -# updated_at :datetime -# state :string(255) -# iid :integer -# - require 'spec_helper' describe Milestone, models: true do @@ -34,6 +19,14 @@ describe Milestone, models: true do let(:issue) { create(:issue) } let(:user) { create(:user) } + describe "#title" do + let(:milestone) { create(:milestone, title: "<b>test</b>") } + + it "sanitizes title" do + expect(milestone.title).to eq("test") + end + end + describe "unique milestone title per project" do it "shouldn't accept the same title in a project twice" do new_milestone = Milestone.new(project: milestone.project, title: milestone.title) @@ -211,4 +204,37 @@ describe Milestone, models: true do to eq([milestone]) end end + + describe '.upcoming_ids_by_projects' do + let(:project_1) { create(:empty_project) } + let(:project_2) { create(:empty_project) } + let(:project_3) { create(:empty_project) } + let(:projects) { [project_1, project_2, project_3] } + + let!(:past_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now - 1.day) } + let!(:current_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now + 1.day) } + let!(:future_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now + 2.days) } + + let!(:past_milestone_project_2) { create(:milestone, project: project_2, due_date: Time.now - 1.day) } + let!(:closed_milestone_project_2) { create(:milestone, :closed, project: project_2, due_date: Time.now + 1.day) } + let!(:current_milestone_project_2) { create(:milestone, project: project_2, due_date: Time.now + 2.days) } + + let!(:past_milestone_project_3) { create(:milestone, project: project_3, due_date: Time.now - 1.day) } + + # The call to `#try` is because this returns a relation with a Postgres DB, + # and an array of IDs with a MySQL DB. + let(:milestone_ids) { Milestone.upcoming_ids_by_projects(projects).map { |id| id.try(:id) || id } } + + it 'returns the next upcoming open milestone ID for each project' do + expect(milestone_ids).to contain_exactly(current_milestone_project_1.id, current_milestone_project_2.id) + end + + context 'when the projects have no open upcoming milestones' do + let(:projects) { [project_3] } + + it 'returns no results' do + expect(milestone_ids).to be_empty + end + end + end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 3c3a580942a..4e68ac5e63a 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -1,18 +1,3 @@ -# == Schema Information -# -# Table name: namespaces -# -# id :integer not null, primary key -# name :string(255) not null -# path :string(255) not null -# owner_id :integer -# created_at :datetime -# updated_at :datetime -# type :string(255) -# description :string(255) default(""), not null -# avatar :string(255) -# - require 'spec_helper' describe Namespace, models: true do @@ -85,6 +70,20 @@ describe Namespace, models: true do allow(@namespace).to receive(:path).and_return(new_path) expect(@namespace.move_dir).to be_truthy end + + context "when any project has container tags" do + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags('tag') + + create(:empty_project, namespace: @namespace) + + allow(@namespace).to receive(:path_was).and_return(@namespace.path) + allow(@namespace).to receive(:path).and_return('new_path') + end + + it { expect { @namespace.move_dir }.to raise_error('Namespace cannot be moved, because at least one project has tags in container registry') } + end end describe :rm_dir do diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 6b18936edb1..f15e96714b2 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -1,24 +1,3 @@ -# == Schema Information -# -# Table name: notes -# -# id :integer not null, primary key -# note :text -# noteable_type :string(255) -# author_id :integer -# created_at :datetime -# updated_at :datetime -# project_id :integer -# attachment :string(255) -# line_code :string(255) -# commit_id :string(255) -# noteable_id :integer -# system :boolean default(FALSE), not null -# st_diff :text -# updated_by_id :integer -# is_award :boolean default(FALSE), not null -# - require 'spec_helper' describe Note, models: true do @@ -30,9 +9,47 @@ describe Note, models: true do it { is_expected.to have_many(:todos).dependent(:destroy) } end + describe 'modules' do + subject { described_class } + + it { is_expected.to include_module(Participable) } + it { is_expected.to include_module(Mentionable) } + it { is_expected.to include_module(Awardable) } + + it { is_expected.to include_module(Gitlab::CurrentSettings) } + end + describe 'validation' do it { is_expected.to validate_presence_of(:note) } it { is_expected.to validate_presence_of(:project) } + + context 'when note is on commit' do + before { allow(subject).to receive(:for_commit?).and_return(true) } + + it { is_expected.to validate_presence_of(:commit_id) } + it { is_expected.not_to validate_presence_of(:noteable_id) } + end + + context 'when note is not on commit' do + before { allow(subject).to receive(:for_commit?).and_return(false) } + + it { is_expected.not_to validate_presence_of(:commit_id) } + it { is_expected.to validate_presence_of(:noteable_id) } + end + + context 'when noteable and note project differ' do + subject do + build(:note, noteable: build_stubbed(:issue), + project: build_stubbed(:project)) + end + + it { is_expected.to be_invalid } + end + + context 'when noteable and note project are the same' do + subject { create(:note) } + it { is_expected.to be_valid } + end end describe "Commit notes" do @@ -55,24 +72,6 @@ describe Note, models: true do end end - describe "Commit diff line notes" do - let!(:note) { create(:note_on_commit_diff, note: "+1 from me") } - let!(:commit) { note.noteable } - - it "should save a valid note" do - expect(note.commit_id).to eq(commit.id) - expect(note.noteable.id).to eq(commit.id) - end - - it "should be recognized by #for_diff_line?" do - expect(note).to be_for_diff_line - end - - it "should be recognized by #for_commit_diff_line?" do - expect(note).to be_for_commit_diff_line - end - end - describe 'authorization' do before do @p1 = create(:project) @@ -128,12 +127,23 @@ describe Note, models: true do end describe "#all_references" do - let!(:note1) { create(:note) } - let!(:note2) { create(:note) } + let!(:note1) { create(:note_on_issue) } + let!(:note2) { create(:note_on_issue) } it "reads the rendered note body from the cache" do - expect(Banzai::Renderer).to receive(:render).with(note1.note, pipeline: :note, cache_key: [note1, "note"], project: note1.project) - expect(Banzai::Renderer).to receive(:render).with(note2.note, pipeline: :note, cache_key: [note2, "note"], project: note2.project) + expect(Banzai::Renderer).to receive(:render). + with(note1.note, + pipeline: :note, + cache_key: [note1, "note"], + project: note1.project, + author: note1.author) + + expect(Banzai::Renderer).to receive(:render). + with(note2.note, + pipeline: :note, + cache_key: [note2, "note"], + project: note2.project, + author: note2.author) note1.all_references note2.all_references @@ -141,7 +151,7 @@ describe Note, models: true do end describe '.search' do - let(:note) { create(:note, note: 'WoW') } + let(:note) { create(:note_on_issue, note: 'WoW') } it 'returns notes with matching content' do expect(described_class.search(note.note)).to eq([note]) @@ -150,81 +160,23 @@ describe Note, models: true do it 'returns notes with matching content regardless of the casing' do expect(described_class.search('WOW')).to eq([note]) end - end - - describe '.grouped_awards' do - before do - create :note, note: "smile", is_award: true - create :note, note: "smile", is_award: true - end - - it "returns grouped hash of notes" do - expect(Note.grouped_awards.keys.size).to eq(3) - expect(Note.grouped_awards["smile"]).to match_array(Note.all) - end - - it "returns thumbsup and thumbsdown always" do - expect(Note.grouped_awards["thumbsup"]).to match_array(Note.none) - expect(Note.grouped_awards["thumbsdown"]).to match_array(Note.none) - end - end - - describe '#active?' do - it 'is always true when the note has no associated diff' do - note = build(:note) - - expect(note).to receive(:diff).and_return(nil) - - expect(note).to be_active - end - - it 'is never true when the note has no noteable associated' do - note = build(:note) - - expect(note).to receive(:diff).and_return(double) - expect(note).to receive(:noteable).and_return(nil) - - expect(note).not_to be_active - end - - it 'returns the memoized value if defined' do - note = build(:note) - - expect(note).to receive(:diff).and_return(double) - expect(note).to receive(:noteable).and_return(double) - - note.instance_variable_set(:@active, 'foo') - expect(note).not_to receive(:find_noteable_diff) - - expect(note.active?).to eq 'foo' - end - - context 'for a merge request noteable' do - it 'is false when noteable has no matching diff' do - merge = build_stubbed(:merge_request, :simple) - note = build(:note, noteable: merge) - allow(note).to receive(:diff).and_return(double) - expect(note).to receive(:find_noteable_diff).and_return(nil) + context "confidential issues" do + let(:user) { create :user } + let(:confidential_issue) { create(:issue, :confidential, author: user) } + let(:confidential_note) { create :note, note: "Random", noteable: confidential_issue, project: confidential_issue.project } - expect(note).not_to be_active + it "returns notes with matching content if user can see the issue" do + expect(described_class.search(confidential_note.note, as_user: user)).to eq([confidential_note]) end - it 'is true when noteable has a matching diff' do - merge = create(:merge_request, :simple) - - # Generate a real line_code value so we know it will match. We use a - # random line from a random diff just for funsies. - diff = merge.diffs.to_a.sample - line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample - code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos) - - # We're persisting in order to trigger the set_diff callback - note = create(:note, noteable: merge, line_code: code) + it "does not return notes with matching content if user can not see the issue" do + user = create :user + expect(described_class.search(confidential_note.note, as_user: user)).to be_empty + end - # Make sure we don't get a false positive from a guard clause - expect(note).to receive(:find_noteable_diff).and_call_original - expect(note).to be_active + it "does not return notes with matching content for unauthenticated users" do + expect(described_class.search(confidential_note.note)).to be_empty end end end @@ -239,11 +191,6 @@ describe Note, models: true do note = build(:note, system: true) expect(note.editable?).to be_falsy end - - it "returns false" do - note = build(:note, is_award: true, note: "smiley") - expect(note.editable?).to be_falsy - end end describe "cross_reference_not_visible_for?" do @@ -270,23 +217,6 @@ describe Note, models: true do end end - describe "set_award!" do - let(:merge_request) { create :merge_request } - - it "converts aliases to actual name" do - note = create(:note, note: ":+1:", noteable: merge_request) - expect(note.reload.note).to eq("thumbsup") - end - - it "is not an award emoji when comment is on a diff" do - note = create(:note, note: ":blowfish:", noteable: merge_request, line_code: "11d5d2e667e9da4f7f610f81d86c974b146b13bd_0_2") - note = note.reload - - expect(note.note).to eq(":blowfish:") - expect(note.is_award?).to be_falsy - end - end - describe 'clear_blank_line_code!' do it 'clears a blank line code before validation' do note = build(:note, line_code: ' ') @@ -294,4 +224,14 @@ describe Note, models: true do expect { note.valid? }.to change(note, :line_code).to(nil) end end + + describe '#participants' do + it 'includes the note author' do + project = create(:project, :public) + issue = create(:issue, project: project) + note = create(:note_on_issue, noteable: issue, project: project) + + expect(note.participants).to include(note.author) + end + end end diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index e771f35811e..ec81f05fc7a 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -194,7 +194,7 @@ describe BambooService, models: true do def service(bamboo_url: 'http://gitlab.com') described_class.create( - project: build_stubbed(:empty_project), + project: create(:empty_project), properties: { bamboo_url: bamboo_url, username: 'mic', diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 6fb5cad5011..5f618322aab 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -176,86 +176,117 @@ describe HipchatService, models: true do context "Note events" do let(:user) { create(:user) } let(:project) { create(:project, creator_id: user.id) } - let(:issue) { create(:issue, project: project) } - let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - let(:snippet) { create(:project_snippet, project: project) } - let(:commit_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } - let(:merge_request_note) { create(:note_on_merge_request, noteable_id: merge_request.id, note: "merge request note") } - let(:issue_note) { create(:note_on_issue, noteable_id: issue.id, note: "issue note")} - let(:snippet_note) { create(:note_on_project_snippet, noteable_id: snippet.id, note: "snippet note") } - - it "should call Hipchat API for commit comment events" do - data = Gitlab::NoteDataBuilder.build(commit_note, user) - hipchat.execute(data) - expect(WebMock).to have_requested(:post, api_url).once + context 'when commit comment event triggered' do + let(:commit_note) do + create(:note_on_commit, author: user, project: project, + commit_id: project.repository.commit.id, + note: 'a comment on a commit') + end + + it "should call Hipchat API for commit comment events" do + data = Gitlab::NoteDataBuilder.build(commit_note, user) + hipchat.execute(data) - message = hipchat.send(:create_message, data) + expect(WebMock).to have_requested(:post, api_url).once - obj_attr = data[:object_attributes] - commit_id = Commit.truncate_sha(data[:commit][:id]) - title = hipchat.send(:format_title, data[:commit][:message]) + message = hipchat.send(:create_message, data) - expect(message).to eq("#{user.name} commented on " \ - "<a href=\"#{obj_attr[:url]}\">commit #{commit_id}</a> in " \ - "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ - "#{title}" \ - "<pre>a comment on a commit</pre>") + obj_attr = data[:object_attributes] + commit_id = Commit.truncate_sha(data[:commit][:id]) + title = hipchat.send(:format_title, data[:commit][:message]) + + expect(message).to eq("#{user.name} commented on " \ + "<a href=\"#{obj_attr[:url]}\">commit #{commit_id}</a> in " \ + "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ + "#{title}" \ + "<pre>a comment on a commit</pre>") + end end - it "should call Hipchat API for merge request comment events" do - data = Gitlab::NoteDataBuilder.build(merge_request_note, user) - hipchat.execute(data) + context 'when merge request comment event triggered' do + let(:merge_request) do + create(:merge_request, source_project: project, + target_project: project) + end - expect(WebMock).to have_requested(:post, api_url).once + let(:merge_request_note) do + create(:note_on_merge_request, noteable: merge_request, + project: project, + note: "merge request note") + end - message = hipchat.send(:create_message, data) + it "should call Hipchat API for merge request comment events" do + data = Gitlab::NoteDataBuilder.build(merge_request_note, user) + hipchat.execute(data) - obj_attr = data[:object_attributes] - merge_id = data[:merge_request]['iid'] - title = data[:merge_request]['title'] + expect(WebMock).to have_requested(:post, api_url).once - expect(message).to eq("#{user.name} commented on " \ - "<a href=\"#{obj_attr[:url]}\">merge request !#{merge_id}</a> in " \ - "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ - "<b>#{title}</b>" \ - "<pre>merge request note</pre>") + message = hipchat.send(:create_message, data) + + obj_attr = data[:object_attributes] + merge_id = data[:merge_request]['iid'] + title = data[:merge_request]['title'] + + expect(message).to eq("#{user.name} commented on " \ + "<a href=\"#{obj_attr[:url]}\">merge request !#{merge_id}</a> in " \ + "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ + "<b>#{title}</b>" \ + "<pre>merge request note</pre>") + end end - it "should call Hipchat API for issue comment events" do - data = Gitlab::NoteDataBuilder.build(issue_note, user) - hipchat.execute(data) + context 'when issue comment event triggered' do + let(:issue) { create(:issue, project: project) } + let(:issue_note) do + create(:note_on_issue, noteable: issue, project: project, + note: "issue note") + end - message = hipchat.send(:create_message, data) + it "should call Hipchat API for issue comment events" do + data = Gitlab::NoteDataBuilder.build(issue_note, user) + hipchat.execute(data) - obj_attr = data[:object_attributes] - issue_id = data[:issue]['iid'] - title = data[:issue]['title'] + message = hipchat.send(:create_message, data) - expect(message).to eq("#{user.name} commented on " \ - "<a href=\"#{obj_attr[:url]}\">issue ##{issue_id}</a> in " \ - "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ - "<b>#{title}</b>" \ - "<pre>issue note</pre>") + obj_attr = data[:object_attributes] + issue_id = data[:issue]['iid'] + title = data[:issue]['title'] + + expect(message).to eq("#{user.name} commented on " \ + "<a href=\"#{obj_attr[:url]}\">issue ##{issue_id}</a> in " \ + "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ + "<b>#{title}</b>" \ + "<pre>issue note</pre>") + end end - it "should call Hipchat API for snippet comment events" do - data = Gitlab::NoteDataBuilder.build(snippet_note, user) - hipchat.execute(data) + context 'when snippet comment event triggered' do + let(:snippet) { create(:project_snippet, project: project) } + let(:snippet_note) do + create(:note_on_project_snippet, noteable: snippet, + project: project, + note: "snippet note") + end - expect(WebMock).to have_requested(:post, api_url).once + it "should call Hipchat API for snippet comment events" do + data = Gitlab::NoteDataBuilder.build(snippet_note, user) + hipchat.execute(data) - message = hipchat.send(:create_message, data) + expect(WebMock).to have_requested(:post, api_url).once - obj_attr = data[:object_attributes] - snippet_id = data[:snippet]['id'] - title = data[:snippet]['title'] + message = hipchat.send(:create_message, data) - expect(message).to eq("#{user.name} commented on " \ - "<a href=\"#{obj_attr[:url]}\">snippet ##{snippet_id}</a> in " \ - "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ - "<b>#{title}</b>" \ - "<pre>snippet note</pre>") + obj_attr = data[:object_attributes] + snippet_id = data[:snippet]['id'] + title = data[:snippet]['title'] + + expect(message).to eq("#{user.name} commented on " \ + "<a href=\"#{obj_attr[:url]}\">snippet ##{snippet_id}</a> in " \ + "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ + "<b>#{title}</b>" \ + "<pre>snippet note</pre>") + end end end @@ -303,7 +334,7 @@ describe HipchatService, models: true do it "should notify only broken" do hipchat.notify_only_broken_builds = true hipchat.execute(data) - expect(WebMock).to_not have_requested(:post, api_url).once + expect(WebMock).not_to have_requested(:post, api_url).once end end end diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/slack_service/build_message_spec.rb index 621c83c0cda..7fcfdf0eacd 100644 --- a/spec/models/project_services/slack_service/build_message_spec.rb +++ b/spec/models/project_services/slack_service/build_message_spec.rb @@ -15,7 +15,7 @@ describe SlackService::BuildMessage do commit: { status: status, author_name: 'hacker', - duration: 10, + duration: duration, }, } end @@ -23,9 +23,10 @@ describe SlackService::BuildMessage do context 'succeeded' do let(:status) { 'success' } let(:color) { 'good' } - + let(:duration) { 10 } + it 'returns a message with information about succeeded build' do - message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker passed in 10 second(s)' + message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker passed in 10 seconds' expect(subject.pretext).to be_empty expect(subject.fallback).to eq(message) expect(subject.attachments).to eq([text: message, color: color]) @@ -35,9 +36,23 @@ describe SlackService::BuildMessage do context 'failed' do let(:status) { 'failed' } let(:color) { 'danger' } + let(:duration) { 10 } it 'returns a message with information about failed build' do - message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 10 second(s)' + message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 10 seconds' + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + end + + describe '#seconds_name' do + let(:status) { 'failed' } + let(:color) { 'danger' } + let(:duration) { 1 } + + it 'returns seconds as singular when there is only one' do + message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 1 second' expect(subject.pretext).to be_empty expect(subject.fallback).to eq(message) expect(subject.attachments).to eq([text: message, color: color]) diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb index f648cbe2dee..0f8889bdf3c 100644 --- a/spec/models/project_services/slack_service/issue_message_spec.rb +++ b/spec/models/project_services/slack_service/issue_message_spec.rb @@ -25,7 +25,7 @@ describe SlackService::IssueMessage, models: true do } end - let(:color) { '#345' } + let(:color) { '#C95823' } context '#initialize' do before do @@ -40,10 +40,11 @@ describe SlackService::IssueMessage, models: true do context 'open' do it 'returns a message regarding opening of issues' do expect(subject.pretext).to eq( - 'Test User opened <url|issue #100> in <somewhere.com|project_name>: '\ - '*Issue title*') + '<somewhere.com|[project_name>] Issue opened by Test User') expect(subject.attachments).to eq([ { + title: "#100 Issue title", + title_link: "url", text: "issue description", color: color, } @@ -56,10 +57,10 @@ describe SlackService::IssueMessage, models: true do args[:object_attributes][:action] = 'close' args[:object_attributes][:state] = 'closed' end + it 'returns a message regarding closing of issues' do expect(subject.pretext). to eq( - 'Test User closed <url|issue #100> in <somewhere.com|project_name>: '\ - '*Issue title*') + '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by Test User') expect(subject.attachments).to be_empty end end diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb index d37590cab75..379c3e1219c 100644 --- a/spec/models/project_services/slack_service/note_message_spec.rb +++ b/spec/models/project_services/slack_service/note_message_spec.rb @@ -65,7 +65,7 @@ describe SlackService::NoteMessage, models: true do expect(message.pretext).to eq("Test User commented on " \ "<url|merge request !30> in <somewhere.com|project_name>: " \ "*merge request title*") - expected_attachments = [ + expected_attachments = [ { text: "comment on a merge request", color: color, @@ -117,7 +117,7 @@ describe SlackService::NoteMessage, models: true do expect(message.pretext).to eq("Test User commented on " \ "<url|snippet #5> in <somewhere.com|project_name>: " \ "*snippet title*") - expected_attachments = [ + expected_attachments = [ { text: "comment on a snippet", color: color, diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index a97b7560137..155f3e74e0d 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -142,13 +142,6 @@ describe SlackService, models: true do let(:slack) { SlackService.new } let(:user) { create(:user) } let(:project) { create(:project, creator_id: user.id) } - let(:issue) { create(:issue, project: project) } - let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - let(:snippet) { create(:project_snippet, project: project) } - let(:commit_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } - let(:merge_request_note) { create(:note_on_merge_request, noteable_id: merge_request.id, note: "merge request note") } - let(:issue_note) { create(:note_on_issue, noteable_id: issue.id, note: "issue note")} - let(:snippet_note) { create(:note_on_project_snippet, noteable_id: snippet.id, note: "snippet note") } let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } before do @@ -162,32 +155,61 @@ describe SlackService, models: true do WebMock.stub_request(:post, webhook_url) end - it "should call Slack API for commit comment events" do - data = Gitlab::NoteDataBuilder.build(commit_note, user) - slack.execute(data) + context 'when commit comment event executed' do + let(:commit_note) do + create(:note_on_commit, author: user, + project: project, + commit_id: project.repository.commit.id, + note: 'a comment on a commit') + end - expect(WebMock).to have_requested(:post, webhook_url).once + it "should call Slack API for commit comment events" do + data = Gitlab::NoteDataBuilder.build(commit_note, user) + slack.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end end - it "should call Slack API for merge request comment events" do - data = Gitlab::NoteDataBuilder.build(merge_request_note, user) - slack.execute(data) + context 'when merge request comment event executed' do + let(:merge_request_note) do + create(:note_on_merge_request, project: project, + note: "merge request note") + end - expect(WebMock).to have_requested(:post, webhook_url).once + it "should call Slack API for merge request comment events" do + data = Gitlab::NoteDataBuilder.build(merge_request_note, user) + slack.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end end - it "should call Slack API for issue comment events" do - data = Gitlab::NoteDataBuilder.build(issue_note, user) - slack.execute(data) + context 'when issue comment event executed' do + let(:issue_note) do + create(:note_on_issue, project: project, note: "issue note") + end - expect(WebMock).to have_requested(:post, webhook_url).once + it "should call Slack API for issue comment events" do + data = Gitlab::NoteDataBuilder.build(issue_note, user) + slack.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end end - it "should call Slack API for snippet comment events" do - data = Gitlab::NoteDataBuilder.build(snippet_note, user) - slack.execute(data) + context 'when snippet comment event executed' do + let(:snippet_note) do + create(:note_on_project_snippet, project: project, + note: "snippet note") + end - expect(WebMock).to have_requested(:post, webhook_url).once + it "should call Slack API for snippet comment events" do + data = Gitlab::NoteDataBuilder.build(snippet_note, user) + slack.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end end end end diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index ad24b895170..24a708ca849 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -182,7 +182,7 @@ describe TeamcityService, models: true do def service(teamcity_url: 'http://gitlab.com') described_class.create( - project: build_stubbed(:empty_project), + project: create(:empty_project), properties: { teamcity_url: teamcity_url, username: 'mic', diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb index e0feb606f78..d9d7c0b0aaa 100644 --- a/spec/models/project_snippet_spec.rb +++ b/spec/models/project_snippet_spec.rb @@ -1,19 +1,3 @@ -# == Schema Information -# -# Table name: snippets -# -# id :integer not null, primary key -# title :string(255) -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string(255) -# type :string(255) -# visibility_level :integer default(0), not null -# - require 'spec_helper' describe ProjectSnippet, models: true do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 3a8a6c2c166..f3590f72cfe 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1,42 +1,3 @@ -# == Schema Information -# -# Table name: projects -# -# id :integer not null, primary key -# name :string(255) -# path :string(255) -# description :text -# created_at :datetime -# updated_at :datetime -# creator_id :integer -# issues_enabled :boolean default(TRUE), not null -# merge_requests_enabled :boolean default(TRUE), not null -# wiki_enabled :boolean default(TRUE), not null -# namespace_id :integer -# issues_tracker :string(255) default("gitlab"), not null -# issues_tracker_id :string(255) -# snippets_enabled :boolean default(TRUE), not null -# last_activity_at :datetime -# import_url :string(255) -# visibility_level :integer default(0), not null -# archived :boolean default(FALSE), not null -# avatar :string(255) -# import_status :string(255) -# repository_size :float default(0.0) -# star_count :integer default(0), not null -# import_type :string(255) -# import_source :string(255) -# commit_count :integer default(0) -# import_error :text -# ci_id :integer -# builds_enabled :boolean default(TRUE), not null -# shared_runners_enabled :boolean default(TRUE), not null -# runners_token :string -# build_coverage_regex :string -# build_allow_git_fetch :boolean default(TRUE), not null -# build_timeout :integer default(3600), not null -# - require 'spec_helper' describe Project, models: true do @@ -61,7 +22,7 @@ describe Project, models: true do it { is_expected.to have_one(:pushover_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) } it { is_expected.to have_many(:commit_statuses) } - it { is_expected.to have_many(:ci_commits) } + it { is_expected.to have_many(:pipelines) } it { is_expected.to have_many(:builds) } it { is_expected.to have_many(:runner_projects) } it { is_expected.to have_many(:runners) } @@ -99,7 +60,7 @@ describe Project, models: true do project2 = build(:project) allow(project2).to receive(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object) expect(project2).not_to be_valid - expect(project2.errors[:limit_reached].first).to match(/Your project limit is 0/) + expect(project2.errors[:limit_reached].first).to match(/Personal project creation is not allowed/) end end @@ -297,6 +258,69 @@ describe Project, models: true do end end + describe :external_issue_tracker do + let(:project) { create(:project) } + let(:ext_project) { create(:redmine_project) } + + context 'on existing projects with no value for has_external_issue_tracker' do + before(:each) do + project.update_column(:has_external_issue_tracker, nil) + ext_project.update_column(:has_external_issue_tracker, nil) + end + + it 'updates the has_external_issue_tracker boolean' do + expect do + project.external_issue_tracker + end.to change { project.reload.has_external_issue_tracker }.to(false) + + expect do + ext_project.external_issue_tracker + end.to change { ext_project.reload.has_external_issue_tracker }.to(true) + end + end + + it 'returns nil and does not query services when there is no external issue tracker' do + project.build_missing_services + project.reload + + expect(project).not_to receive(:services) + + expect(project.external_issue_tracker).to eq(nil) + end + + it 'retrieves external_issue_tracker querying services and cache it when there is external issue tracker' do + ext_project.reload # Factory returns a project with changed attributes + ext_project.build_missing_services + ext_project.reload + + expect(ext_project).to receive(:services).once.and_call_original + + 2.times { expect(ext_project.external_issue_tracker).to be_a_kind_of(RedmineService) } + end + end + + describe :cache_has_external_issue_tracker do + let(:project) { create(:project) } + + it 'stores true if there is any external_issue_tracker' do + services = double(:service, external_issue_trackers: [RedmineService.new]) + expect(project).to receive(:services).and_return(services) + + expect do + project.cache_has_external_issue_tracker + end.to change { project.has_external_issue_tracker}.to(true) + end + + it 'stores false if there is no external_issue_tracker' do + services = double(:service, external_issue_trackers: []) + expect(project).to receive(:services).and_return(services) + + expect do + project.cache_has_external_issue_tracker + end.to change { project.has_external_issue_tracker}.to(false) + end + end + describe :can_have_issues_tracker_id? do let(:project) { create(:project) } let(:ext_project) { create(:redmine_project) } @@ -438,23 +462,23 @@ describe Project, models: true do end end - describe :ci_commit do + describe :pipeline do let(:project) { create :project } - let(:commit) { create :ci_commit, project: project, ref: 'master' } + let(:pipeline) { create :ci_pipeline, project: project, ref: 'master' } - subject { project.ci_commit(commit.sha, 'master') } + subject { project.pipeline(pipeline.sha, 'master') } - it { is_expected.to eq(commit) } + it { is_expected.to eq(pipeline) } context 'return latest' do - let(:commit2) { create :ci_commit, project: project, ref: 'master' } + let(:pipeline2) { create :ci_pipeline, project: project, ref: 'master' } before do - commit - commit2 + pipeline + pipeline2 end - it { is_expected.to eq(commit2) } + it { is_expected.to eq(pipeline2) } end end @@ -673,11 +697,11 @@ describe Project, models: true do # Project#gitlab_shell returns a new instance of Gitlab::Shell on every # call. This makes testing a bit easier. allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) - end - it 'renames a repository' do allow(project).to receive(:previous_changes).and_return('path' => ['foo']) + end + it 'renames a repository' do ns = project.namespace_dir expect(gitlab_shell).to receive(:mv_repository). @@ -702,6 +726,17 @@ describe Project, models: true do project.rename_repo end + + context 'container registry with tags' do + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags('tag') + end + + subject { project.rename_repo } + + it { expect{subject}.to raise_error(Exception) } + end end describe '#expire_caches_before_rename' do @@ -811,4 +846,113 @@ describe Project, models: true do expect(project.protected_branch?('foo')).to eq(false) end end + + describe '#container_registry_path_with_namespace' do + let(:project) { create(:empty_project, path: 'PROJECT') } + + subject { project.container_registry_path_with_namespace } + + it { is_expected.not_to eq(project.path_with_namespace) } + it { is_expected.to eq(project.path_with_namespace.downcase) } + end + + describe '#container_registry_repository' do + let(:project) { create(:empty_project) } + + before { stub_container_registry_config(enabled: true) } + + subject { project.container_registry_repository } + + it { is_expected.not_to be_nil } + end + + describe '#container_registry_repository_url' do + let(:project) { create(:empty_project) } + + subject { project.container_registry_repository_url } + + before { stub_container_registry_config(**registry_settings) } + + context 'for enabled registry' do + let(:registry_settings) do + { + enabled: true, + host_port: 'example.com', + } + end + + it { is_expected.not_to be_nil } + end + + context 'for disabled registry' do + let(:registry_settings) do + { + enabled: false + } + end + + it { is_expected.to be_nil } + end + end + + describe '#has_container_registry_tags?' do + let(:project) { create(:empty_project) } + + subject { project.has_container_registry_tags? } + + context 'for enabled registry' do + before { stub_container_registry_config(enabled: true) } + + context 'with tags' do + before { stub_container_registry_tags('test', 'test2') } + + it { is_expected.to be_truthy } + end + + context 'when no tags' do + before { stub_container_registry_tags } + + it { is_expected.to be_falsey } + end + end + + context 'for disabled registry' do + before { stub_container_registry_config(enabled: false) } + + it { is_expected.to be_falsey } + end + end + + describe '.where_paths_in' do + context 'without any paths' do + it 'returns an empty relation' do + expect(Project.where_paths_in([])).to eq([]) + end + end + + context 'without any valid paths' do + it 'returns an empty relation' do + expect(Project.where_paths_in(%w[foo])).to eq([]) + end + end + + context 'with valid paths' do + let!(:project1) { create(:project) } + let!(:project2) { create(:project) } + + it 'returns the projects matching the paths' do + projects = Project.where_paths_in([project1.path_with_namespace, + project2.path_with_namespace]) + + expect(projects).to contain_exactly(project1, project2) + end + + it 'returns projects regardless of the casing of paths' do + projects = Project.where_paths_in([project1.path_with_namespace.upcase, + project2.path_with_namespace.upcase]) + + expect(projects).to contain_exactly(project1, project2) + end + end + end end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 532e3f013fd..58b57bd4fef 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -16,6 +16,12 @@ describe ProjectWiki, models: true do end end + describe '#web_url' do + it 'returns the full web URL to the wiki' do + expect(subject.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/wikis/home") + end + end + describe "#url_to_repo" do it "returns the correct ssh url to the repo" do expect(subject.url_to_repo).to eq(gitlab_shell.url_to_repo(subject.path_with_namespace)) @@ -38,7 +44,8 @@ describe ProjectWiki, models: true do describe "#wiki_base_path" do it "returns the wiki base path" do - wiki_base_path = "/#{project.path_with_namespace}/wikis" + wiki_base_path = "#{Gitlab.config.gitlab.relative_url_root}/#{project.path_with_namespace}/wikis" + expect(subject.wiki_base_path).to eq(wiki_base_path) end end @@ -256,6 +263,13 @@ describe ProjectWiki, models: true do end end + describe '#hook_attrs' do + it 'returns a hash with values' do + expect(subject.hook_attrs).to be_a Hash + expect(subject.hook_attrs.keys).to contain_exactly(:web_url, :git_ssh_url, :git_http_url, :path_with_namespace, :default_branch) + end + end + private def create_temp_repo(path) diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb index 7e956cf6779..b523834c6e9 100644 --- a/spec/models/protected_branch_spec.rb +++ b/spec/models/protected_branch_spec.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: protected_branches -# -# id :integer not null, primary key -# project_id :integer not null -# name :string(255) not null -# created_at :datetime -# updated_at :datetime -# developers_can_push :boolean default(FALSE), not null -# - require 'spec_helper' describe ProtectedBranch, models: true do diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb index 72ecb442a36..527005b2b69 100644 --- a/spec/models/release_spec.rb +++ b/spec/models/release_spec.rb @@ -1,15 +1,3 @@ -# == Schema Information -# -# Table name: releases -# -# id :integer not null, primary key -# tag :string(255) -# description :text -# project_id :integer -# created_at :datetime -# updated_at :datetime -# - require 'rails_helper' RSpec.describe Release, type: :model do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 34a13f9b5c9..8c2347992f1 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -100,6 +100,12 @@ describe Repository, models: true do expect(results.first).not_to start_with('fatal:') end + it 'properly handles an unmatched parenthesis' do + results = repository.search_files("test(", 'master') + + expect(results.first).not_to start_with('fatal:') + end + describe 'result' do subject { results.first } @@ -176,6 +182,15 @@ describe Repository, models: true do repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') end + it 'handles when HEAD points to non-existent ref' do + repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) + rugged = double('rugged') + expect(rugged).to receive(:head_unborn?).and_return(true) + expect(repository).to receive(:rugged).and_return(rugged) + + expect(repository.license_blob).to be_nil + end + it 'looks in the root_ref only' do repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'markdown') repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'markdown', false) @@ -204,6 +219,15 @@ describe Repository, models: true do repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') end + it 'handles when HEAD points to non-existent ref' do + repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) + rugged = double('rugged') + expect(rugged).to receive(:head_unborn?).and_return(true) + expect(repository).to receive(:rugged).and_return(rugged) + + expect(repository.license_key).to be_nil + end + it 'returns nil when no license is detected' do expect(repository.license_key).to be_nil end @@ -419,7 +443,7 @@ describe Repository, models: true do end it 'does nothing' do - expect(repository.raw_repository).to_not receive(:autocrlf=). + expect(repository.raw_repository).not_to receive(:autocrlf=). with(:input) repository.update_autocrlf_option @@ -487,7 +511,7 @@ describe Repository, models: true do it 'does not expire the emptiness caches for a non-empty repository' do expect(repository).to receive(:empty?).and_return(false) - expect(repository).to_not receive(:expire_emptiness_caches) + expect(repository).not_to receive(:expire_emptiness_caches) repository.expire_cache end @@ -650,7 +674,7 @@ describe Repository, models: true do end it 'does not flush caches that depend on repository data' do - expect(repository).to_not receive(:expire_cache) + expect(repository).not_to receive(:expire_cache) repository.before_delete end @@ -805,18 +829,6 @@ describe Repository, models: true do end end - describe "#main_language" do - it 'shows the main language of the project' do - expect(repository.main_language).to eq("Ruby") - end - - it 'returns nil when the repository is empty' do - allow(repository).to receive(:empty?).and_return(true) - - expect(repository.main_language).to be_nil - end - end - describe '#before_remove_tag' do it 'flushes the tag cache' do expect(repository).to receive(:expire_tag_count_cache) @@ -927,7 +939,7 @@ describe Repository, models: true do expect(repository.avatar).to eq('logo.png') - expect(repository).to_not receive(:blob_at_branch) + expect(repository).not_to receive(:blob_at_branch) expect(repository.avatar).to eq('logo.png') end end @@ -1021,7 +1033,7 @@ describe Repository, models: true do and_return(true) repository.cache_keys.each do |key| - expect(repository).to_not receive(key) + expect(repository).not_to receive(key) end repository.build_cache diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 173628c08d0..2f000dbc01a 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -1,24 +1,3 @@ -# == Schema Information -# -# Table name: services -# -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean default(TRUE) -# issues_events :boolean default(TRUE) -# merge_requests_events :boolean default(TRUE) -# tag_push_events :boolean default(TRUE) -# note_events :boolean default(TRUE), not null -# build_events :boolean default(FALSE), not null -# - require 'spec_helper' describe Service, models: true do @@ -225,4 +204,37 @@ describe Service, models: true do expect(service.bamboo_url_was).to be_nil end end + + describe "callbacks" do + let(:project) { create(:project) } + let!(:service) do + RedmineService.new( + project: project, + active: true, + properties: { + project_url: 'http://redmine/projects/project_name_in_redmine', + issues_url: "http://redmine/#{project.id}/project_name_in_redmine/:id", + new_issue_url: 'http://redmine/projects/project_name_in_redmine/issues/new' + } + ) + end + + describe "on create" do + it "updates the has_external_issue_tracker boolean" do + expect do + service.save! + end.to change { service.project.has_external_issue_tracker }.from(nil).to(true) + end + end + + describe "on update" do + it "updates the has_external_issue_tracker boolean" do + service.save! + + expect do + service.update_attributes(active: false) + end.to change { service.project.has_external_issue_tracker }.from(true).to(false) + end + end + end end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 5077ac7b62b..789816bf2c7 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -1,19 +1,3 @@ -# == Schema Information -# -# Table name: snippets -# -# id :integer not null, primary key -# title :string(255) -# content :text -# author_id :integer not null -# project_id :integer -# created_at :datetime -# updated_at :datetime -# file_name :string(255) -# type :string(255) -# visibility_level :integer default(0), not null -# - require 'spec_helper' describe Snippet, models: true do @@ -103,4 +87,31 @@ describe Snippet, models: true do expect(described_class.search_code('FOO')).to eq([snippet]) end end + + describe '#participants' do + let(:project) { create(:project, :public) } + let(:snippet) { create(:snippet, content: 'foo', project: project) } + + let!(:note1) do + create(:note_on_project_snippet, + noteable: snippet, + project: project, + note: 'a') + end + + let!(:note2) do + create(:note_on_project_snippet, + noteable: snippet, + project: project, + note: 'b') + end + + it 'includes the snippet author' do + expect(snippet.participants).to include(snippet.author) + end + + it 'includes the note authors' do + expect(snippet.participants).to include(note1.author, note2.author) + end + end end diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index d9b86b9368f..623b82c01d8 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -1,21 +1,3 @@ -# == Schema Information -# -# Table name: todos -# -# id :integer not null, primary key -# user_id :integer not null -# project_id :integer not null -# target_id :integer -# target_type :string not null -# author_id :integer -# action :integer not null -# state :string not null -# created_at :datetime -# updated_at :datetime -# note_id :integer -# commit_id :string -# - require 'spec_helper' describe Todo, models: true do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 8b2fb77e28e..73bee535fe3 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,66 +1,3 @@ -# == Schema Information -# -# Table name: users -# -# id :integer not null, primary key -# email :string(255) default(""), not null -# encrypted_password :string(255) default(""), not null -# reset_password_token :string(255) -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string(255) -# last_sign_in_ip :string(255) -# created_at :datetime -# updated_at :datetime -# name :string(255) -# admin :boolean default(FALSE), not null -# projects_limit :integer default(10) -# skype :string(255) default(""), not null -# linkedin :string(255) default(""), not null -# twitter :string(255) default(""), not null -# authentication_token :string(255) -# theme_id :integer default(1), not null -# bio :string(255) -# failed_attempts :integer default(0) -# locked_at :datetime -# username :string(255) -# can_create_group :boolean default(TRUE), not null -# can_create_team :boolean default(TRUE), not null -# state :string(255) -# color_scheme_id :integer default(1), not null -# notification_level :integer default(1), not null -# password_expires_at :datetime -# created_by_id :integer -# last_credential_check_at :datetime -# avatar :string(255) -# confirmation_token :string(255) -# confirmed_at :datetime -# confirmation_sent_at :datetime -# unconfirmed_email :string(255) -# hide_no_ssh_key :boolean default(FALSE) -# website_url :string(255) default(""), not null -# notification_email :string(255) -# hide_no_password :boolean default(FALSE) -# password_automatically_set :boolean default(FALSE) -# location :string(255) -# encrypted_otp_secret :string(255) -# encrypted_otp_secret_iv :string(255) -# encrypted_otp_secret_salt :string(255) -# otp_required_for_login :boolean default(FALSE), not null -# otp_backup_codes :text -# public_email :string(255) default(""), not null -# dashboard :integer default(0) -# project_view :integer default(0) -# consumed_timestep :integer -# layout :integer default(0) -# hide_project_limit :boolean default(FALSE) -# unlock_token :string -# otp_grace_period_started_at :datetime -# - require 'spec_helper' describe User, models: true do @@ -93,6 +30,7 @@ describe User, models: true do it { is_expected.to have_one(:abuse_report) } it { is_expected.to have_many(:spam_logs).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) } + it { is_expected.to have_many(:award_emoji).dependent(:destroy) } end describe 'validations' do @@ -130,7 +68,10 @@ describe User, models: true do describe 'email' do context 'when no signup domains listed' do - before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return([]) } + before do + allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return([]) + end + it 'accepts any email' do user = build(:user, email: "info@example.com") expect(user).to be_valid @@ -138,7 +79,10 @@ describe User, models: true do end context 'when a signup domain is listed and subdomains are allowed' do - before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com']) } + before do + allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com']) + end + it 'accepts info@example.com' do user = build(:user, email: "info@example.com") expect(user).to be_valid @@ -156,7 +100,9 @@ describe User, models: true do end context 'when a signup domain is listed and subdomains are not allowed' do - before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return(['example.com']) } + before do + allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com']) + end it 'accepts info@example.com' do user = build(:user, email: "info@example.com") @@ -183,6 +129,66 @@ describe User, models: true do end end + describe "scopes" do + describe ".with_two_factor" do + it "returns users with 2fa enabled via OTP" do + user_with_2fa = create(:user, :two_factor_via_otp) + user_without_2fa = create(:user) + users_with_two_factor = User.with_two_factor.pluck(:id) + + expect(users_with_two_factor).to include(user_with_2fa.id) + expect(users_with_two_factor).not_to include(user_without_2fa.id) + end + + it "returns users with 2fa enabled via U2F" do + user_with_2fa = create(:user, :two_factor_via_u2f) + user_without_2fa = create(:user) + users_with_two_factor = User.with_two_factor.pluck(:id) + + expect(users_with_two_factor).to include(user_with_2fa.id) + expect(users_with_two_factor).not_to include(user_without_2fa.id) + end + + it "returns users with 2fa enabled via OTP and U2F" do + user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f) + user_without_2fa = create(:user) + users_with_two_factor = User.with_two_factor.pluck(:id) + + expect(users_with_two_factor).to eq([user_with_2fa.id]) + expect(users_with_two_factor).not_to include(user_without_2fa.id) + end + end + + describe ".without_two_factor" do + it "excludes users with 2fa enabled via OTP" do + user_with_2fa = create(:user, :two_factor_via_otp) + user_without_2fa = create(:user) + users_without_two_factor = User.without_two_factor.pluck(:id) + + expect(users_without_two_factor).to include(user_without_2fa.id) + expect(users_without_two_factor).not_to include(user_with_2fa.id) + end + + it "excludes users with 2fa enabled via U2F" do + user_with_2fa = create(:user, :two_factor_via_u2f) + user_without_2fa = create(:user) + users_without_two_factor = User.without_two_factor.pluck(:id) + + expect(users_without_two_factor).to include(user_without_2fa.id) + expect(users_without_two_factor).not_to include(user_with_2fa.id) + end + + it "excludes users with 2fa enabled via OTP and U2F" do + user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f) + user_without_2fa = create(:user) + users_without_two_factor = User.without_two_factor.pluck(:id) + + expect(users_without_two_factor).to include(user_without_2fa.id) + expect(users_without_two_factor).not_to include(user_with_2fa.id) + end + end + end + describe "Respond to" do it { is_expected.to respond_to(:is_admin?) } it { is_expected.to respond_to(:name) } @@ -204,6 +210,10 @@ describe User, models: true do end describe '#confirm' do + before do + allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) + end + let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'test@gitlab.com') } it 'returns unconfirmed' do @@ -845,4 +855,92 @@ describe User, models: true do it { is_expected.to eq([private_project]) } end + + describe '#ci_authorized_runners' do + let(:user) { create(:user) } + let(:runner) { create(:ci_runner) } + + before do + project.runners << runner + end + + context 'without any projects' do + let(:project) { create(:project) } + + it 'does not load' do + expect(user.ci_authorized_runners).to be_empty + end + end + + context 'with personal projects runners' do + let(:namespace) { create(:namespace, owner: user) } + let(:project) { create(:project, namespace: namespace) } + + it 'loads' do + expect(user.ci_authorized_runners).to contain_exactly(runner) + end + end + + shared_examples :member do + context 'when the user is a master' do + before do + add_user(Gitlab::Access::MASTER) + end + + it 'loads' do + expect(user.ci_authorized_runners).to contain_exactly(runner) + end + end + + context 'when the user is a developer' do + before do + add_user(Gitlab::Access::DEVELOPER) + end + + it 'does not load' do + expect(user.ci_authorized_runners).to be_empty + end + end + end + + context 'with groups projects runners' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + + def add_user(access) + group.add_user(user, access) + end + + it_behaves_like :member + end + + context 'with other projects runners' do + let(:project) { create(:project) } + + def add_user(access) + project.team << [user, access] + end + + it_behaves_like :member + end + end + + describe '#viewable_starred_projects' do + let(:user) { create(:user) } + let(:public_project) { create(:empty_project, :public) } + let(:private_project) { create(:empty_project, :private) } + let(:private_viewable_project) { create(:empty_project, :private) } + + before do + private_viewable_project.team << [user, Gitlab::Access::MASTER] + + [public_project, private_project, private_viewable_project].each do |project| + user.toggle_star(project) + end + end + + it 'returns only starred projects the user can view' do + expect(user.viewable_starred_projects).not_to include(private_project) + end + end end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 5ead735be48..6cb7be188ef 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -9,8 +9,8 @@ describe API::API, api: true do let!(:project) { create(:project, creator_id: user.id) } let!(:developer) { create(:project_member, :developer, user: user, project: project) } let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) } - let(:commit) { create(:ci_commit, project: project)} - let(:build) { create(:ci_build, commit: commit) } + let(:pipeline) { create(:ci_pipeline, project: project)} + let(:build) { create(:ci_build, pipeline: pipeline) } describe 'GET /projects/:id/builds ' do let(:query) { '' } @@ -59,8 +59,8 @@ describe API::API, api: true do describe 'GET /projects/:id/repository/commits/:sha/builds' do before do - project.ensure_ci_commit(commit.sha, 'master') - get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds", api_user) + project.ensure_pipeline(pipeline.sha, 'master') + get api("/projects/#{project.id}/repository/commits/#{pipeline.sha}/builds", api_user) end context 'authorized user' do @@ -102,12 +102,12 @@ describe API::API, api: true do before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) } context 'build with artifacts' do - let(:build) { create(:ci_build, :artifacts, commit: commit) } + let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } context 'authorized user' do let(:download_headers) do - { 'Content-Transfer-Encoding'=>'binary', - 'Content-Disposition'=>'attachment; filename=ci_build_artifacts.zip' } + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } end it 'should return specific build artifacts' do @@ -131,7 +131,7 @@ describe API::API, api: true do end describe 'GET /projects/:id/builds/:build_id/trace' do - let(:build) { create(:ci_build, :trace, commit: commit) } + let(:build) { create(:ci_build, :trace, pipeline: pipeline) } before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) } @@ -181,7 +181,7 @@ describe API::API, api: true do end describe 'POST /projects/:id/builds/:build_id/retry' do - let(:build) { create(:ci_build, :canceled, commit: commit) } + let(:build) { create(:ci_build, :canceled, pipeline: pipeline) } before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) } @@ -218,7 +218,7 @@ describe API::API, api: true do end context 'build is erasable' do - let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, commit: commit) } + let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) } it 'should erase build content' do expect(response.status).to eq 201 @@ -234,7 +234,7 @@ describe API::API, api: true do end context 'build is not erasable' do - let(:build) { create(:ci_build, :trace, project: project, commit: commit) } + let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) } it 'should respond with forbidden' do expect(response.status).to eq 403 diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_statuses_spec.rb index f3785b19362..298cdbad329 100644 --- a/spec/requests/api/commit_status_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' -describe API::CommitStatus, api: true do +describe API::CommitStatuses, api: true do include ApiHelpers let!(:project) { create(:project) } let(:commit) { project.repository.commit } - let(:commit_status) { create(:commit_status, commit: ci_commit) } + let(:commit_status) { create(:commit_status, pipeline: pipeline) } let(:guest) { create_user(:guest) } let(:reporter) { create_user(:reporter) } let(:developer) { create_user(:developer) } @@ -16,8 +16,8 @@ describe API::CommitStatus, api: true do let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" } context 'ci commit exists' do - let!(:master) { project.ci_commits.create(sha: commit.id, ref: 'master') } - let!(:develop) { project.ci_commits.create(sha: commit.id, ref: 'develop') } + let!(:master) { project.pipelines.create(sha: commit.id, ref: 'master') } + let!(:develop) { project.pipelines.create(sha: commit.id, ref: 'develop') } it_behaves_like 'a paginated resources' do let(:request) { get api(get_url, reporter) } @@ -27,7 +27,7 @@ describe API::CommitStatus, api: true do let(:statuses_id) { json_response.map { |status| status['id'] } } def create_status(commit, opts = {}) - create(:commit_status, { commit: commit, ref: commit.ref }.merge(opts)) + create(:commit_status, { pipeline: commit, ref: commit.ref }.merge(opts)) end let!(:status1) { create_status(master, status: 'running') } diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index cb82ca7802d..6fc38f537d3 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -90,10 +90,10 @@ describe API::API, api: true do end it "should return status for CI" do - ci_commit = project.ensure_ci_commit(project.repository.commit.sha, 'master') + pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master') get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) expect(response.status).to eq(200) - expect(json_response['status']).to eq(ci_commit.status) + expect(json_response['status']).to eq(pipeline.status) end end diff --git a/spec/requests/api/gitignores_spec.rb b/spec/requests/api/gitignores_spec.rb new file mode 100644 index 00000000000..aab2d8c81b9 --- /dev/null +++ b/spec/requests/api/gitignores_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe API::Gitignores, api: true do + include ApiHelpers + + describe 'Entity Gitignore' do + before { get api('/gitignores/Ruby') } + + it { expect(json_response['name']).to eq('Ruby') } + it { expect(json_response['content']).to include('*.gem') } + end + + describe 'Entity GitignoresList' do + before { get api('/gitignores') } + + it { expect(json_response.first['name']).not_to be_nil } + it { expect(json_response.first['content']).to be_nil } + end + + describe 'GET /gitignores' do + it 'returns a list of available license templates' do + get api('/gitignores') + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.size).to be > 15 + end + end +end diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb index 96d89e69209..02553d0f8e2 100644 --- a/spec/requests/api/group_members_spec.rb +++ b/spec/requests/api/group_members_spec.rb @@ -34,11 +34,11 @@ describe API::API, api: true do expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.size).to eq(5) - expect(json_response.find { |e| e['id']==owner.id }['access_level']).to eq(GroupMember::OWNER) - expect(json_response.find { |e| e['id']==reporter.id }['access_level']).to eq(GroupMember::REPORTER) - expect(json_response.find { |e| e['id']==developer.id }['access_level']).to eq(GroupMember::DEVELOPER) - expect(json_response.find { |e| e['id']==master.id }['access_level']).to eq(GroupMember::MASTER) - expect(json_response.find { |e| e['id']==guest.id }['access_level']).to eq(GroupMember::GUEST) + expect(json_response.find { |e| e['id'] == owner.id }['access_level']).to eq(GroupMember::OWNER) + expect(json_response.find { |e| e['id'] == reporter.id }['access_level']).to eq(GroupMember::REPORTER) + expect(json_response.find { |e| e['id'] == developer.id }['access_level']).to eq(GroupMember::DEVELOPER) + expect(json_response.find { |e| e['id'] == master.id }['access_level']).to eq(GroupMember::MASTER) + expect(json_response.find { |e| e['id'] == guest.id }['access_level']).to eq(GroupMember::GUEST) end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 37ddab83c30..7ecefce80d6 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -12,6 +12,7 @@ describe API::API, api: true do let!(:group2) { create(:group, :private) } let!(:project1) { create(:project, namespace: group1) } let!(:project2) { create(:project, namespace: group2) } + let!(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) } before do group1.add_owner(user1) @@ -147,9 +148,11 @@ describe API::API, api: true do context "when authenticated as user" do it "should return the group's projects" do get api("/groups/#{group1.id}/projects", user1) + expect(response.status).to eq(200) - expect(json_response.length).to eq(1) - expect(json_response.first['name']).to eq(project1.name) + expect(json_response.length).to eq(2) + project_names = json_response.map { |proj| proj['name' ] } + expect(project_names).to match_array([project1.name, project3.name]) end it "should not return a non existing group" do @@ -162,6 +165,16 @@ describe API::API, api: true do expect(response.status).to eq(404) end + + it "should only return projects to which user has access" do + project3.team << [user3, :developer] + + get api("/groups/#{group1.id}/projects", user3) + + expect(response.status).to eq(200) + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(project3.name) + end end context "when authenticated as admin" do @@ -181,8 +194,10 @@ describe API::API, api: true do context 'when using group path in URL' do it 'should return any existing group' do get api("/groups/#{group1.path}/projects", admin) + expect(response.status).to eq(200) - expect(json_response.first['name']).to eq(project1.name) + project_names = json_response.map { |proj| proj['name' ] } + expect(project_names).to match_array([project1.name, project3.name]) end it 'should not return a non existing group' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index f88e39cad9e..bb926172593 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -39,6 +39,7 @@ describe API::API, api: true do let!(:empty_milestone) do create(:milestone, title: '2.0.0', project: project) end + let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } before { project.team << [user, :reporter] } @@ -232,8 +233,27 @@ describe API::API, api: true do end describe "GET /projects/:id/issues/:issue_id" do + it 'exposes known attributes' do + get api("/projects/#{project.id}/issues/#{issue.id}", user) + + expect(response.status).to eq(200) + expect(json_response['id']).to eq(issue.id) + expect(json_response['iid']).to eq(issue.iid) + expect(json_response['project_id']).to eq(issue.project.id) + expect(json_response['title']).to eq(issue.title) + expect(json_response['description']).to eq(issue.description) + expect(json_response['state']).to eq(issue.state) + expect(json_response['created_at']).to be_present + expect(json_response['updated_at']).to be_present + expect(json_response['labels']).to eq(issue.label_names) + expect(json_response['milestone']).to be_a Hash + expect(json_response['assignee']).to be_a Hash + expect(json_response['author']).to be_a Hash + end + it "should return a project issue by id" do get api("/projects/#{project.id}/issues/#{issue.id}", user) + expect(response.status).to eq(200) expect(json_response['title']).to eq(issue.title) expect(json_response['iid']).to eq(issue.iid) @@ -602,6 +622,12 @@ describe API::API, api: true do expect(response.status).to eq(404) end + + it 'returns 404 if the issue is confidential' do + post api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) + + expect(response.status).to eq(404) + end end describe 'DELETE :id/issues/:issue_id/subscription' do @@ -623,5 +649,11 @@ describe API::API, api: true do expect(response.status).to eq(404) end + + it 'returns 404 if the issue is confidential' do + delete api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) + + expect(response.status).to eq(404) + end end end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 6943ff9d26c..b2c7f8d9acb 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -190,4 +190,86 @@ describe API::API, api: true do expect(json_response['message']['color']).to eq(['must be a valid color code']) end end + + describe "POST /projects/:id/labels/:label_id/subscription" do + context "when label_id is a label title" do + it "should subscribe to the label" do + post api("/projects/#{project.id}/labels/#{label1.title}/subscription", user) + + expect(response.status).to eq(201) + expect(json_response["name"]).to eq(label1.title) + expect(json_response["subscribed"]).to be_truthy + end + end + + context "when label_id is a label ID" do + it "should subscribe to the label" do + post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + + expect(response.status).to eq(201) + expect(json_response["name"]).to eq(label1.title) + expect(json_response["subscribed"]).to be_truthy + end + end + + context "when user is already subscribed to label" do + before { label1.subscribe(user) } + + it "should return 304" do + post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + + expect(response.status).to eq(304) + end + end + + context "when label ID is not found" do + it "should a return 404 error" do + post api("/projects/#{project.id}/labels/1234/subscription", user) + + expect(response.status).to eq(404) + end + end + end + + describe "DELETE /projects/:id/labels/:label_id/subscription" do + before { label1.subscribe(user) } + + context "when label_id is a label title" do + it "should unsubscribe from the label" do + delete api("/projects/#{project.id}/labels/#{label1.title}/subscription", user) + + expect(response.status).to eq(200) + expect(json_response["name"]).to eq(label1.title) + expect(json_response["subscribed"]).to be_falsey + end + end + + context "when label_id is a label ID" do + it "should unsubscribe from the label" do + delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + + expect(response.status).to eq(200) + expect(json_response["name"]).to eq(label1.title) + expect(json_response["subscribed"]).to be_falsey + end + end + + context "when user is already unsubscribed from label" do + before { label1.unsubscribe(user) } + + it "should return 304" do + delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user) + + expect(response.status).to eq(304) + end + end + + context "when label ID is not found" do + it "should a return 404 error" do + delete api("/projects/#{project.id}/labels/1234/subscription", user) + + expect(response.status).to eq(404) + end + end + end end diff --git a/spec/requests/api/licenses_spec.rb b/spec/requests/api/licenses_spec.rb index c17dcb222a9..3726b2f5688 100644 --- a/spec/requests/api/licenses_spec.rb +++ b/spec/requests/api/licenses_spec.rb @@ -57,7 +57,7 @@ describe API::Licenses, api: true do end it 'replaces placeholder values' do - expect(json_response['content']).to include('Copyright (c) 2016 Anton') + expect(json_response['content']).to include("Copyright (c) #{Time.now.year} Anton") end end @@ -70,7 +70,7 @@ describe API::Licenses, api: true do it 'replaces placeholder values' do expect(json_response['content']).to include('My Awesome Project') - expect(json_response['content']).to include('Copyright (C) 2016 Anton') + expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") end end @@ -83,7 +83,7 @@ describe API::Licenses, api: true do it 'replaces placeholder values' do expect(json_response['content']).to include('My Awesome Project') - expect(json_response['content']).to include('Copyright (C) 2016 Anton') + expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") end end @@ -96,7 +96,7 @@ describe API::Licenses, api: true do it 'replaces placeholder values' do expect(json_response['content']).to include('My Awesome Project') - expect(json_response['content']).to include('Copyright (C) 2016 Anton') + expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton") end end @@ -108,7 +108,7 @@ describe API::Licenses, api: true do end it 'replaces placeholder values' do - expect(json_response['content']).to include('Copyright 2016 Anton') + expect(json_response['content']).to include("Copyright #{Time.now.year} Anton") end end @@ -128,7 +128,7 @@ describe API::Licenses, api: true do it 'replaces the copyright owner placeholder with the name of the current user' do get api('/licenses/mit', user) - expect(json_response['content']).to include("Copyright (c) 2016 #{user.name}") + expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}") end end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 1fa7e76894f..5896b93603f 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -113,6 +113,33 @@ describe API::API, api: true do end describe "GET /projects/:id/merge_requests/:merge_request_id" do + it 'exposes known attributes' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) + + expect(response.status).to eq(200) + expect(json_response['id']).to eq(merge_request.id) + expect(json_response['iid']).to eq(merge_request.iid) + expect(json_response['project_id']).to eq(merge_request.project.id) + expect(json_response['title']).to eq(merge_request.title) + expect(json_response['description']).to eq(merge_request.description) + expect(json_response['state']).to eq(merge_request.state) + expect(json_response['created_at']).to be_present + expect(json_response['updated_at']).to be_present + expect(json_response['labels']).to eq(merge_request.label_names) + expect(json_response['milestone']).to be_nil + expect(json_response['assignee']).to be_a Hash + expect(json_response['author']).to be_a Hash + expect(json_response['target_branch']).to eq(merge_request.target_branch) + expect(json_response['source_branch']).to eq(merge_request.source_branch) + expect(json_response['upvotes']).to eq(0) + expect(json_response['downvotes']).to eq(0) + expect(json_response['source_project_id']).to eq(merge_request.source_project.id) + expect(json_response['target_project_id']).to eq(merge_request.target_project.id) + expect(json_response['work_in_progress']).to be_falsy + expect(json_response['merge_when_build_succeeds']).to be_falsy + expect(json_response['merge_status']).to eq('can_be_merged') + end + it "should return merge_request" do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) expect(response.status).to eq(200) @@ -360,7 +387,7 @@ describe API::API, api: true do end describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do - let(:ci_commit) { create(:ci_commit_without_jobs) } + let(:pipeline) { create(:ci_pipeline_without_jobs) } it "should return merge_request in case of success" do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) @@ -392,6 +419,15 @@ describe API::API, api: true do expect(json_response['message']).to eq('405 Method Not Allowed') end + it 'returns 405 if the build failed for a merge request that requires success' do + allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false) + + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + + expect(response.status).to eq(405) + expect(json_response['message']).to eq('405 Method Not Allowed') + end + it "should return 401 if user has no permissions to merge" do user2 = create(:user) project.team << [user2, :reporter] @@ -400,9 +436,22 @@ describe API::API, api: true do expect(json_response['message']).to eq('401 Unauthorized') end + it "returns 409 if the SHA parameter doesn't match" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.source_sha.succ + + expect(response.status).to eq(409) + expect(json_response['message']).to start_with('SHA does not match HEAD of source branch') + end + + it "succeeds if the SHA parameter matches" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.source_sha + + expect(response.status).to eq(200) + end + it "enables merge when build succeeds if the ci is active" do - allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) - allow(ci_commit).to receive(:active?).and_return(true) + allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline) + allow(pipeline).to receive(:active?).and_return(true) put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true @@ -514,6 +563,21 @@ describe API::API, api: true do expect(json_response).to be_an Array expect(json_response.length).to eq(0) end + + it 'handles external issues' do + jira_project = create(:jira_project, :public, name: 'JIR_EXT1') + issue = ExternalIssue.new("#{jira_project.name}-123", jira_project) + merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project) + merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}") + + get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['title']).to eq(issue.title) + expect(json_response.first['id']).to eq(issue.id) + end end describe 'POST :id/merge_requests/:merge_request_id/subscription' do diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 49091fc0f49..beb29a68692 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } - let!(:project) { create(:project, namespace: user.namespace) } + let!(:project) { create(:project, :public, namespace: user.namespace) } let!(:issue) { create(:issue, project: project, author: user) } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } let!(:snippet) { create(:project_snippet, project: project, author: user) } @@ -39,6 +39,7 @@ describe API::API, api: true do context "when noteable is an Issue" do it "should return an array of issue notes" do get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.first['body']).to eq(issue_note.note) @@ -46,20 +47,33 @@ describe API::API, api: true do it "should return a 404 error when issue id not found" do get api("/projects/#{project.id}/issues/12345/notes", user) + expect(response.status).to eq(404) end - context "that references a private issue" do + context "and current user cannot view the notes" do it "should return an empty array" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response).to be_empty end + context "and issue is confidential" do + before { ext_issue.update_attributes(confidential: true) } + + it "returns 404" do + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + + expect(response.status).to eq(404) + end + end + context "and current user can view the note" do it "should return an empty array" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user) + expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.first['body']).to eq(cross_reference_note.note) @@ -71,6 +85,7 @@ describe API::API, api: true do context "when noteable is a Snippet" do it "should return an array of snippet notes" do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) + expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.first['body']).to eq(snippet_note.note) @@ -78,6 +93,13 @@ describe API::API, api: true do it "should return a 404 error when snippet id not found" do get api("/projects/#{project.id}/snippets/42/notes", user) + + expect(response.status).to eq(404) + end + + it "returns 404 when not authorized" do + get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", private_user) + expect(response.status).to eq(404) end end @@ -85,6 +107,7 @@ describe API::API, api: true do context "when noteable is a Merge Request" do it "should return an array of merge_requests notes" do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user) + expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.first['body']).to eq(merge_request_note.note) @@ -92,6 +115,13 @@ describe API::API, api: true do it "should return a 404 error if merge request id not found" do get api("/projects/#{project.id}/merge_requests/4444/notes", user) + + expect(response.status).to eq(404) + end + + it "returns 404 when not authorized" do + get api("/projects/#{project.id}/merge_requests/4444/notes", private_user) + expect(response.status).to eq(404) end end @@ -101,24 +131,39 @@ describe API::API, api: true do context "when noteable is an Issue" do it "should return an issue note by id" do get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user) + expect(response.status).to eq(200) expect(json_response['body']).to eq(issue_note.note) end it "should return a 404 error if issue note not found" do get api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) + expect(response.status).to eq(404) end - context "that references a private issue" do + context "and current user cannot view the note" do it "should return a 404 error" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user) + expect(response.status).to eq(404) end + context "when issue is confidential" do + before { issue.update_attributes(confidential: true) } + + it "returns 404" do + get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user) + + expect(response.status).to eq(404) + end + end + + context "and current user can view the note" do it "should return an issue note by id" do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user) + expect(response.status).to eq(200) expect(json_response['body']).to eq(cross_reference_note.note) end @@ -129,12 +174,14 @@ describe API::API, api: true do context "when noteable is a Snippet" do it "should return a snippet note by id" do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user) + expect(response.status).to eq(200) expect(json_response['body']).to eq(snippet_note.note) end it "should return a 404 error if snippet note not found" do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user) + expect(response.status).to eq(404) end end @@ -144,6 +191,7 @@ describe API::API, api: true do context "when noteable is an Issue" do it "should create a new issue note" do post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' + expect(response.status).to eq(201) expect(json_response['body']).to eq('hi!') expect(json_response['author']['username']).to eq(user.username) @@ -151,11 +199,13 @@ describe API::API, api: true do it "should return a 400 bad request error if body not given" do post api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + expect(response.status).to eq(400) end it "should return a 401 unauthorized error if user not authenticated" do post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!' + expect(response.status).to eq(401) end @@ -164,6 +214,7 @@ describe API::API, api: true do creation_time = 2.weeks.ago post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!', created_at: creation_time + expect(response.status).to eq(201) expect(json_response['body']).to eq('hi!') expect(json_response['author']['username']).to eq(user.username) @@ -176,6 +227,7 @@ describe API::API, api: true do context "when noteable is a Snippet" do it "should create a new snippet note" do post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!' + expect(response.status).to eq(201) expect(json_response['body']).to eq('hi!') expect(json_response['author']['username']).to eq(user.username) @@ -183,11 +235,13 @@ describe API::API, api: true do it "should return a 400 bad request error if body not given" do post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) + expect(response.status).to eq(400) end it "should return a 401 unauthorized error if user not authenticated" do post api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!' + expect(response.status).to eq(401) end end @@ -204,8 +258,8 @@ describe API::API, api: true do body: 'Hi!' end - it 'responds with 500' do - expect(response.status).to eq 500 + it 'responds with resource not found error' do + expect(response.status).to eq 404 end it 'does not create new note' do @@ -227,6 +281,7 @@ describe API::API, api: true do it 'should return modified note' do put api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user), body: 'Hello!' + expect(response.status).to eq(200) expect(json_response['body']).to eq('Hello!') end @@ -234,12 +289,14 @@ describe API::API, api: true do it 'should return a 404 error when note id not found' do put api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user), body: 'Hello!' + expect(response.status).to eq(404) end it 'should return a 400 bad request error if body not given' do put api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user) + expect(response.status).to eq(400) end end @@ -248,6 +305,7 @@ describe API::API, api: true do it 'should return modified note' do put api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/#{snippet_note.id}", user), body: 'Hello!' + expect(response.status).to eq(200) expect(json_response['body']).to eq('Hello!') end @@ -255,6 +313,7 @@ describe API::API, api: true do it 'should return a 404 error when note id not found' do put api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/12345", user), body: "Hello!" + expect(response.status).to eq(404) end end @@ -263,6 +322,7 @@ describe API::API, api: true do it 'should return modified note' do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ "notes/#{merge_request_note.id}", user), body: 'Hello!' + expect(response.status).to eq(200) expect(json_response['body']).to eq('Hello!') end @@ -270,6 +330,7 @@ describe API::API, api: true do it 'should return a 404 error when note id not found' do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ "notes/12345", user), body: "Hello!" + expect(response.status).to eq(404) end end diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb index c112ca5e3ca..44b532b10e1 100644 --- a/spec/requests/api/project_members_spec.rb +++ b/spec/requests/api/project_members_spec.rb @@ -133,7 +133,7 @@ describe API::API, api: true do delete api("/projects/#{project.id}/members/#{user3.id}", user) expect do delete api("/projects/#{project.id}/members/#{user3.id}", user) - end.to_not change { ProjectMember.count } + end.not_to change { ProjectMember.count } expect(response.status).to eq(200) end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 66193eac051..f167813e07d 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -10,20 +10,20 @@ describe API::API, api: true do let(:admin) { create(:admin) } let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } let(:project2) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace) } - let(:project3) { create(:project, path: 'project3', creator_id: user.id, namespace: user.namespace) } let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } let(:project_member) { create(:project_member, :master, user: user, project: project) } let(:project_member2) { create(:project_member, :developer, user: user3, project: project) } let(:user4) { create(:user) } let(:project3) do create(:project, + :private, name: 'second_project', path: 'second_project', creator_id: user.id, namespace: user.namespace, merge_requests_enabled: false, issues_enabled: false, wiki_enabled: false, - snippets_enabled: false, visibility_level: 0) + snippets_enabled: false) end let(:project_member3) do create(:project_member, @@ -164,21 +164,18 @@ describe API::API, api: true do end describe 'GET /projects/starred' do + let(:public_project) { create(:project, :public) } + before do - admin.starred_projects << project - admin.save! + project_member2 + user3.update_attributes(starred_projects: [project, project2, project3, public_project]) end - it 'should return the starred projects' do - get api('/projects/all', admin) + it 'should return the starred projects viewable by the user' do + get api('/projects/starred', user3) expect(response.status).to eq(200) expect(json_response).to be_an Array - - expect(json_response).to satisfy do |response| - response.one? do |entry| - entry['name'] == project.name - end - end + expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id) end end diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 3af61d4b335..73ae8ef631c 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -184,21 +184,24 @@ describe API::Runners, api: true do description = shared_runner.description active = shared_runner.active - put api("/runners/#{shared_runner.id}", admin), description: "#{description}_updated", active: !active, - tag_list: ['ruby2.1', 'pgsql', 'mysql'] + update_runner(shared_runner.id, admin, description: "#{description}_updated", + active: !active, + tag_list: ['ruby2.1', 'pgsql', 'mysql'], + run_untagged: 'false') shared_runner.reload expect(response.status).to eq(200) expect(shared_runner.description).to eq("#{description}_updated") expect(shared_runner.active).to eq(!active) expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql') + expect(shared_runner.run_untagged?).to be false end end context 'when runner is not shared' do it 'should update runner' do description = specific_runner.description - put api("/runners/#{specific_runner.id}", admin), description: 'test' + update_runner(specific_runner.id, admin, description: 'test') specific_runner.reload expect(response.status).to eq(200) @@ -208,10 +211,14 @@ describe API::Runners, api: true do end it 'should return 404 if runner does not exists' do - put api('/runners/9999', admin), description: 'test' + update_runner(9999, admin, description: 'test') expect(response.status).to eq(404) end + + def update_runner(id, user, args) + put api("/runners/#{id}", user), args + end end context 'authorized user' do diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index 3e676515488..94eebc48ec8 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -49,7 +49,7 @@ describe API::API, api: true do it "should not create new hook without url" do expect do post api("/hooks", admin) - end.to_not change { SystemHook.count } + end.not_to change { SystemHook.count } end end diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 0510b77a39b..fdd4ec6d761 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -23,7 +23,7 @@ describe API::API do end before do - stub_ci_commit_to_return_yaml_file + stub_ci_pipeline_to_return_yaml_file end context 'Handles errors' do @@ -44,13 +44,13 @@ describe API::API do end context 'Have a commit' do - let(:commit) { project.ci_commits.last } + let(:pipeline) { project.pipelines.last } it 'should create builds' do post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master') expect(response.status).to eq(201) - commit.builds.reload - expect(commit.builds.size).to eq(2) + pipeline.builds.reload + expect(pipeline.builds.size).to eq(2) end it 'should return bad request with no builds created if there\'s no commit for that ref' do @@ -79,8 +79,8 @@ describe API::API do it 'create trigger request with variables' do post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master') expect(response.status).to eq(201) - commit.builds.reload - expect(commit.builds.first.trigger_request.variables).to eq(variables) + pipeline.builds.reload + expect(pipeline.builds.first.trigger_request.variables).to eq(variables) end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 40b24c125b5..a7690f430c4 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -20,7 +20,7 @@ describe API::API, api: true do end context "when authenticated" do - #These specs are written just in case API authentication is not required anymore + # These specs are written just in case API authentication is not required anymore context "when public level is restricted" do before do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index dfd361a2cdd..e8508f8f950 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -7,7 +7,7 @@ describe Ci::API::API do let(:project) { FactoryGirl.create(:empty_project) } before do - stub_ci_commit_to_return_yaml_file + stub_ci_pipeline_to_return_yaml_file end describe "Builds API for runners" do @@ -20,9 +20,9 @@ describe Ci::API::API do describe "POST /builds/register" do it "should start a build" do - commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') - commit.create_builds(nil) - build = commit.builds.first + pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') + pipeline.create_builds(nil) + build = pipeline.builds.first post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -38,8 +38,8 @@ describe Ci::API::API do end it "should return 404 error if no builds for specific runner" do - commit = FactoryGirl.create(:ci_commit, project: shared_project) - FactoryGirl.create(:ci_build, commit: commit, status: 'pending') + pipeline = FactoryGirl.create(:ci_pipeline, project: shared_project) + FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending') post ci_api("/builds/register"), token: runner.token @@ -47,8 +47,8 @@ describe Ci::API::API do end it "should return 404 error if no builds for shared runner" do - commit = FactoryGirl.create(:ci_commit, project: project) - FactoryGirl.create(:ci_build, commit: commit, status: 'pending') + pipeline = FactoryGirl.create(:ci_pipeline, project: project) + FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending') post ci_api("/builds/register"), token: shared_runner.token @@ -56,8 +56,8 @@ describe Ci::API::API do end it "returns options" do - commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') - commit.create_builds(nil) + pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') + pipeline.create_builds(nil) post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -66,8 +66,8 @@ describe Ci::API::API do end it "returns variables" do - commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') - commit.create_builds(nil) + pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') + pipeline.create_builds(nil) project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -83,10 +83,10 @@ describe Ci::API::API do it "returns variables for triggers" do trigger = FactoryGirl.create(:ci_trigger, project: project) - commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') + pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') - trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger) - commit.create_builds(nil, trigger_request) + trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) + pipeline.create_builds(nil, trigger_request) project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value") post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -103,9 +103,9 @@ describe Ci::API::API do end it "returns dependent builds" do - commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master') - commit.create_builds(nil, nil) - commit.builds.where(stage: 'test').each(&:success) + pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master') + pipeline.create_builds(nil, nil) + pipeline.builds.where(stage: 'test').each(&:success) post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } @@ -128,11 +128,43 @@ describe Ci::API::API do end end end + + context 'when build has no tags' do + before do + pipeline = create(:ci_pipeline, project: project) + create(:ci_build, pipeline: pipeline, tags: []) + end + + context 'when runner is allowed to pick untagged builds' do + before { runner.update_column(:run_untagged, true) } + + it 'picks build' do + register_builds + + expect(response).to have_http_status 201 + end + end + + context 'when runner is not allowed to pick untagged builds' do + before { runner.update_column(:run_untagged, false) } + + it 'does not pick build' do + register_builds + + expect(response).to have_http_status 404 + end + end + + def register_builds + post ci_api("/builds/register"), token: runner.token, + info: { platform: :darwin } + end + end end describe "PUT /builds/:id" do - let(:commit) {create(:ci_commit, project: project)} - let(:build) { create(:ci_build, :trace, commit: commit, runner_id: runner.id) } + let(:pipeline) {create(:ci_pipeline, project: project)} + let(:build) { create(:ci_build, :trace, pipeline: pipeline, runner_id: runner.id) } before do build.run! @@ -205,8 +237,8 @@ describe Ci::API::API do context "Artifacts" do let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } - let(:commit) { create(:ci_commit, project: project) } - let(:build) { create(:ci_build, commit: commit, runner_id: runner.id) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline, runner_id: runner.id) } let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") } let(:post_url) { ci_api("/builds/#{build.id}/artifacts") } let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") } @@ -221,13 +253,13 @@ describe Ci::API::API do it "using token as parameter" do post authorize_url, { token: build.token }, headers expect(response.status).to eq(200) - expect(json_response["TempPath"]).to_not be_nil + expect(json_response["TempPath"]).not_to be_nil end it "using token as header" do post authorize_url, {}, headers_with_token expect(response.status).to eq(200) - expect(json_response["TempPath"]).to_not be_nil + expect(json_response["TempPath"]).not_to be_nil end end @@ -402,8 +434,8 @@ describe Ci::API::API do context 'build has artifacts' do let(:build) { create(:ci_build, :artifacts) } let(:download_headers) do - { 'Content-Transfer-Encoding'=>'binary', - 'Content-Disposition'=>'attachment; filename=ci_build_artifacts.zip' } + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } end it 'should download artifact' do diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index db8189ffb79..43596f07cb5 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -12,44 +12,85 @@ describe Ci::API::API do end describe "POST /runners/register" do - describe "should create a runner if token provided" do + context 'when runner token is provided' do before { post ci_api("/runners/register"), token: registration_token } - it { expect(response.status).to eq(201) } + it 'creates runner with default values' do + expect(response).to have_http_status 201 + expect(Ci::Runner.first.run_untagged).to be true + end end - describe "should create a runner with description" do - before { post ci_api("/runners/register"), token: registration_token, description: "server.hostname" } + context 'when runner description is provided' do + before do + post ci_api("/runners/register"), token: registration_token, + description: "server.hostname" + end - it { expect(response.status).to eq(201) } - it { expect(Ci::Runner.first.description).to eq("server.hostname") } + it 'creates runner' do + expect(response).to have_http_status 201 + expect(Ci::Runner.first.description).to eq("server.hostname") + end end - describe "should create a runner with tags" do - before { post ci_api("/runners/register"), token: registration_token, tag_list: "tag1, tag2" } + context 'when runner tags are provided' do + before do + post ci_api("/runners/register"), token: registration_token, + tag_list: "tag1, tag2" + end - it { expect(response.status).to eq(201) } - it { expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"]) } + it 'creates runner' do + expect(response).to have_http_status 201 + expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"]) + end end - describe "should create a runner if project token provided" do + context 'when option for running untagged jobs is provided' do + context 'when tags are provided' do + it 'creates runner' do + post ci_api("/runners/register"), token: registration_token, + run_untagged: false, + tag_list: ['tag'] + + expect(response).to have_http_status 201 + expect(Ci::Runner.first.run_untagged).to be false + end + end + + context 'when tags are not provided' do + it 'does not create runner' do + post ci_api("/runners/register"), token: registration_token, + run_untagged: false + + expect(response).to have_http_status 404 + end + end + end + + context 'when project token is provided' do let(:project) { FactoryGirl.create(:empty_project) } before { post ci_api("/runners/register"), token: project.runners_token } - it { expect(response.status).to eq(201) } - it { expect(project.runners.size).to eq(1) } + it 'creates runner' do + expect(response).to have_http_status 201 + expect(project.runners.size).to eq(1) + end end - it "should return 403 error if token is invalid" do - post ci_api("/runners/register"), token: 'invalid' + context 'when token is invalid' do + it 'returns 403 error' do + post ci_api("/runners/register"), token: 'invalid' - expect(response.status).to eq(403) + expect(response).to have_http_status 403 + end end - it "should return 400 error if no token" do - post ci_api("/runners/register") + context 'when no token provided' do + it 'returns 400 error' do + post ci_api("/runners/register") - expect(response.status).to eq(400) + expect(response).to have_http_status 400 + end end %w(name version revision platform architecture).each do |param| @@ -60,7 +101,7 @@ describe Ci::API::API do it do post ci_api("/runners/register"), token: registration_token, info: { param => value } - expect(response.status).to eq(201) + expect(response).to have_http_status 201 is_expected.to eq(value) end end @@ -71,7 +112,7 @@ describe Ci::API::API do let!(:runner) { FactoryGirl.create(:ci_runner) } before { delete ci_api("/runners/delete"), token: runner.token } - it { expect(response.status).to eq(200) } + it { expect(response).to have_http_status 200 } it { expect(Ci::Runner.count).to eq(0) } end end diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb index 0ef03f9371b..72f6a3c981d 100644 --- a/spec/requests/ci/api/triggers_spec.rb +++ b/spec/requests/ci/api/triggers_spec.rb @@ -15,7 +15,7 @@ describe Ci::API::API do end before do - stub_ci_commit_to_return_yaml_file + stub_ci_pipeline_to_return_yaml_file end context 'Handles errors' do @@ -36,13 +36,13 @@ describe Ci::API::API do end context 'Have a commit' do - let(:commit) { project.ci_commits.last } + let(:pipeline) { project.pipelines.last } it 'should create builds' do post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options expect(response.status).to eq(201) - commit.builds.reload - expect(commit.builds.size).to eq(2) + pipeline.builds.reload + expect(pipeline.builds.size).to eq(2) end it 'should return bad request with no builds created if there\'s no commit for that ref' do @@ -71,8 +71,8 @@ describe Ci::API::API do it 'create trigger request with variables' do post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: variables) expect(response.status).to eq(201) - commit.builds.reload - expect(commit.builds.first.trigger_request.variables).to eq(variables) + pipeline.builds.reload + expect(pipeline.builds.first.trigger_request.variables).to eq(variables) end end end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb new file mode 100644 index 00000000000..c44a4a7a1fc --- /dev/null +++ b/spec/requests/git_http_spec.rb @@ -0,0 +1,395 @@ +require "spec_helper" + +describe 'Git HTTP requests', lib: true do + let(:user) { create(:user) } + let(:project) { create(:project, path: 'project.git-project') } + + it "gives WWW-Authenticate hints" do + clone_get('doesnt/exist.git') + + expect(response.header['WWW-Authenticate']).to start_with('Basic ') + end + + context "when the project doesn't exist" do + context "when no authentication is provided" do + it "responds with status 401 (no project existence information leak)" do + download('doesnt/exist.git') do |response| + expect(response.status).to eq(401) + end + end + end + + context "when username and password are provided" do + context "when authentication fails" do + it "responds with status 401" do + download('doesnt/exist.git', user: user.username, password: "nope") do |response| + expect(response.status).to eq(401) + end + end + end + + context "when authentication succeeds" do + it "responds with status 404" do + download('/doesnt/exist.git', user: user.username, password: user.password) do |response| + expect(response.status).to eq(404) + end + end + end + end + end + + context "when the Wiki for a project exists" do + it "responds with the right project" do + wiki = ProjectWiki.new(project) + project.update_attribute(:visibility_level, Project::PUBLIC) + + download("/#{wiki.repository.path_with_namespace}.git") do |response| + json_body = ActiveSupport::JSON.decode(response.body) + + expect(response.status).to eq(200) + expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace) + end + end + end + + context "when the project exists" do + let(:path) { "#{project.path_with_namespace}.git" } + + context "when the project is public" do + before do + project.update_attribute(:visibility_level, Project::PUBLIC) + end + + it "downloads get status 200" do + download(path, {}) do |response| + expect(response.status).to eq(200) + end + end + + it "uploads get status 401" do + upload(path, {}) do |response| + expect(response.status).to eq(401) + end + end + + context "with correct credentials" do + let(:env) { { user: user.username, password: user.password } } + + it "uploads get status 200 (because Git hooks do the real check)" do + upload(path, env) do |response| + expect(response.status).to eq(200) + end + end + + context 'but git-receive-pack is disabled' do + it "responds with status 404" do + allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false) + + upload(path, env) do |response| + expect(response.status).to eq(404) + end + end + end + end + + context 'but git-upload-pack is disabled' do + it "responds with status 404" do + allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false) + + download(path, {}) do |response| + expect(response.status).to eq(404) + end + end + end + end + + context "when the project is private" do + before do + project.update_attribute(:visibility_level, Project::PRIVATE) + end + + context "when no authentication is provided" do + it "responds with status 401 to downloads" do + download(path, {}) do |response| + expect(response.status).to eq(401) + end + end + + it "responds with status 401 to uploads" do + upload(path, {}) do |response| + expect(response.status).to eq(401) + end + end + end + + context "when username and password are provided" do + let(:env) { { user: user.username, password: 'nope' } } + + context "when authentication fails" do + it "responds with status 401" do + download(path, env) do |response| + expect(response.status).to eq(401) + end + end + + context "when the user is IP banned" do + it "responds with status 401" do + expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true) + allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4') + + clone_get(path, env) + + expect(response.status).to eq(401) + end + end + end + + context "when authentication succeeds" do + let(:env) { { user: user.username, password: user.password } } + + context "when the user has access to the project" do + before do + project.team << [user, :master] + end + + context "when the user is blocked" do + it "responds with status 404" do + user.block + project.team << [user, :master] + + download(path, env) do |response| + expect(response.status).to eq(404) + end + end + end + + context "when the user isn't blocked" do + it "downloads get status 200" do + expect(Rack::Attack::Allow2Ban).to receive(:reset) + + clone_get(path, env) + + expect(response.status).to eq(200) + end + + it "uploads get status 200" do + upload(path, env) do |response| + expect(response.status).to eq(200) + end + end + end + + context "when an oauth token is provided" do + before do + application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) + @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id) + end + + it "downloads get status 200" do + clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token + + expect(response.status).to eq(200) + end + + it "uploads get status 401 (no project existence information leak)" do + push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token + + expect(response.status).to eq(401) + end + end + + context "when blank password attempts follow a valid login" do + def attempt_login(include_password) + password = include_password ? user.password : "" + clone_get path, user: user.username, password: password + response.status + end + + it "repeated attempts followed by successful attempt" do + options = Gitlab.config.rack_attack.git_basic_auth + maxretry = options[:maxretry] - 1 + ip = '1.2.3.4' + + allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip) + Rack::Attack::Allow2Ban.reset(ip, options) + + maxretry.times.each do + expect(attempt_login(false)).to eq(401) + end + + expect(attempt_login(true)).to eq(200) + expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey + + maxretry.times.each do + expect(attempt_login(false)).to eq(401) + end + + Rack::Attack::Allow2Ban.reset(ip, options) + end + end + end + + context "when the user doesn't have access to the project" do + it "downloads get status 404" do + download(path, user: user.username, password: user.password) do |response| + expect(response.status).to eq(404) + end + end + + it "uploads get status 200 (because Git hooks do the real check)" do + upload(path, user: user.username, password: user.password) do |response| + expect(response.status).to eq(200) + end + end + end + end + end + + context "when a gitlab ci token is provided" do + let(:token) { 123 } + let(:project) { FactoryGirl.create :empty_project } + + before do + project.update_attributes(runners_token: token, builds_enabled: true) + end + + it "downloads get status 200" do + clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token + + expect(response.status).to eq(200) + end + + it "uploads get status 401 (no project existence information leak)" do + push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token + + expect(response.status).to eq(401) + end + end + end + end + + context "when the project path doesn't end in .git" do + context "GET info/refs" do + let(:path) { "/#{project.path_with_namespace}/info/refs" } + + context "when no params are added" do + before { get path } + + it "redirects to the .git suffix version" do + expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs") + end + end + + context "when the upload-pack service is requested" do + let(:params) { { service: 'git-upload-pack' } } + before { get path, params } + + it "redirects to the .git suffix version" do + expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}") + end + end + + context "when the receive-pack service is requested" do + let(:params) { { service: 'git-receive-pack' } } + before { get path, params } + + it "redirects to the .git suffix version" do + expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}") + end + end + + context "when the params are anything else" do + let(:params) { { service: 'git-implode-pack' } } + before { get path, params } + + it "redirects to the sign-in page" do + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context "POST git-upload-pack" do + it "fails to find a route" do + expect { clone_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError) + end + end + + context "POST git-receive-pack" do + it "failes to find a route" do + expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError) + end + end + end + + context "retrieving an info/refs file" do + before { project.update_attribute(:visibility_level, Project::PUBLIC) } + + context "when the file exists" do + before do + # Provide a dummy file in its place + allow_any_instance_of(Repository).to receive(:blob_at).and_call_original + allow_any_instance_of(Repository).to receive(:blob_at).with('5937ac0a7beb003549fc5fd26fc247adbce4a52e', 'info/refs') do + Gitlab::Git::Blob.find(project.repository, 'master', '.gitignore') + end + + get "/#{project.path_with_namespace}/blob/master/info/refs" + end + + it "returns the file" do + expect(response.status).to eq(200) + end + end + + context "when the file exists" do + before { get "/#{project.path_with_namespace}/blob/master/info/refs" } + + it "returns not found" do + expect(response.status).to eq(404) + end + end + end + + def clone_get(project, options={}) + get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password)) + end + + def clone_post(project, options={}) + post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password)) + end + + def push_get(project, options={}) + get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password)) + end + + def push_post(project, options={}) + post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password)) + end + + def download(project, user: nil, password: nil) + args = [project, { user: user, password: password }] + + clone_get(*args) + yield response + + clone_post(*args) + yield response + end + + def upload(project, user: nil, password: nil) + args = [project, { user: user, password: password }] + + push_get(*args) + yield response + + push_post(*args) + yield response + end + + def auth_env(user, password) + if user && password + { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password) } + else + {} + end + end +end diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb new file mode 100644 index 00000000000..c995993a853 --- /dev/null +++ b/spec/requests/jwt_controller_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe JwtController do + let(:service) { double(execute: {}) } + let(:service_class) { double(new: service) } + let(:service_name) { 'test' } + let(:parameters) { { service: service_name } } + + before { stub_const('JwtController::SERVICES', service_name => service_class) } + + context 'existing service' do + subject! { get '/jwt/auth', parameters } + + it { expect(response.status).to eq(200) } + + context 'returning custom http code' do + let(:service) { double(execute: { http_status: 505 }) } + + it { expect(response.status).to eq(505) } + end + end + + context 'when using authorized request' do + context 'using CI token' do + let(:project) { create(:empty_project, runners_token: 'token', builds_enabled: builds_enabled) } + let(:headers) { { authorization: credentials('gitlab-ci-token', project.runners_token) } } + + subject! { get '/jwt/auth', parameters, headers } + + context 'project with enabled CI' do + let(:builds_enabled) { true } + + it { expect(service_class).to have_received(:new).with(project, nil, parameters) } + end + + context 'project with disabled CI' do + let(:builds_enabled) { false } + + it { expect(response.status).to eq(403) } + end + end + + context 'using User login' do + let(:user) { create(:user) } + let(:headers) { { authorization: credentials('user', 'password') } } + + before { expect(Gitlab::Auth).to receive(:find_in_gitlab_or_ldap).with('user', 'password').and_return(user) } + + subject! { get '/jwt/auth', parameters, headers } + + it { expect(service_class).to have_received(:new).with(nil, user, parameters) } + end + + context 'using invalid login' do + let(:headers) { { authorization: credentials('invalid', 'password') } } + + subject! { get '/jwt/auth', parameters, headers } + + it { expect(response.status).to eq(403) } + end + end + + context 'unknown service' do + subject! { get '/jwt/auth', service: 'unknown' } + + it { expect(response.status).to eq(404) } + end + + def credentials(login, password) + ActionController::HttpAuthentication::Basic.encode_credentials(login, password) + end +end diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index cd16a8e6322..b5ed8584c8a 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -118,3 +118,10 @@ describe Admin::DashboardController, "routing" do expect(get("/admin")).to route_to('admin/dashboard#index') end end + +# admin_health_check GET /admin/health_check(.:format) admin/health_check#show +describe Admin::HealthCheckController, "routing" do + it "to #show" do + expect(get("/admin/health_check")).to route_to('admin/health_check#show') + end +end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 1527eddfa48..de13c0db5d1 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -1,5 +1,42 @@ require 'spec_helper' +# user GET /u/:username/ +# user_groups GET /u/:username/groups(.:format) +# user_projects GET /u/:username/projects(.:format) +# user_contributed_projects GET /u/:username/contributed(.:format) +# user_snippets GET /u/:username/snippets(.:format) +# user_calendar GET /u/:username/calendar(.:format) +# user_calendar_activities GET /u/:username/calendar_activities(.:format) +describe UsersController, "routing" do + it "to #show" do + expect(get("/u/User")).to route_to('users#show', username: 'User') + end + + it "to #groups" do + expect(get("/u/User/groups")).to route_to('users#groups', username: 'User') + end + + it "to #projects" do + expect(get("/u/User/projects")).to route_to('users#projects', username: 'User') + end + + it "to #contributed" do + expect(get("/u/User/contributed")).to route_to('users#contributed', username: 'User') + end + + it "to #snippets" do + expect(get("/u/User/snippets")).to route_to('users#snippets', username: 'User') + end + + it "to #calendar" do + expect(get("/u/User/calendar")).to route_to('users#calendar', username: 'User') + end + + it "to #calendar_activities" do + expect(get("/u/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User') + end +end + # search GET /search(.:format) search#show describe SearchController, "routing" do it "to #show" do @@ -27,10 +64,6 @@ end # PUT /snippets/:id(.:format) snippets#update # DELETE /snippets/:id(.:format) snippets#destroy describe SnippetsController, "routing" do - it "to #user_index" do - expect(get("/s/User")).to route_to('snippets#index', username: 'User') - end - it "to #raw" do expect(get("/snippets/1/raw")).to route_to('snippets#raw', id: '1') end @@ -243,3 +276,13 @@ describe "Groups", "routing" do expect(get('/1')).to route_to('namespaces#show', id: '1') end end + +describe HealthCheckController, 'routing' do + it 'to #index' do + expect(get('/health_check')).to route_to('health_check#index') + end + + it 'also supports passing checks in the url' do + expect(get('/health_check/email')).to route_to('health_check#index', checks: 'email') + end +end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb new file mode 100644 index 00000000000..67777ad48bc --- /dev/null +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -0,0 +1,242 @@ +require 'spec_helper' + +describe Auth::ContainerRegistryAuthenticationService, services: true do + let(:current_project) { nil } + let(:current_user) { nil } + let(:current_params) { {} } + let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) } + let(:payload) { JWT.decode(subject[:token], rsa_key).first } + + subject { described_class.new(current_project, current_user, current_params).execute } + + before do + allow(Gitlab.config.registry).to receive_messages(enabled: true, issuer: 'rspec', key: nil) + allow_any_instance_of(JSONWebToken::RSAToken).to receive(:key).and_return(rsa_key) + end + + shared_examples 'a valid token' do + it { is_expected.to include(:token) } + it { expect(payload).to include('access') } + + context 'a expirable' do + let(:expires_at) { Time.at(payload['exp']) } + let(:expire_delay) { 10 } + + context 'for default configuration' do + it { expect(expires_at).not_to be_within(2.seconds).of(Time.now + expire_delay.minutes) } + end + + context 'for changed configuration' do + before { stub_application_setting(container_registry_token_expire_delay: expire_delay) } + + it { expect(expires_at).to be_within(2.seconds).of(Time.now + expire_delay.minutes) } + end + end + end + + shared_examples 'a accessible' do + let(:access) do + [{ + 'type' => 'repository', + 'name' => project.path_with_namespace, + 'actions' => actions, + }] + end + + it_behaves_like 'a valid token' + it { expect(payload).to include('access' => access) } + end + + shared_examples 'an inaccessible' do + it_behaves_like 'a valid token' + it { expect(payload).to include('access' => []) } + end + + shared_examples 'a pullable' do + it_behaves_like 'a accessible' do + let(:actions) { ['pull'] } + end + end + + shared_examples 'a pushable' do + it_behaves_like 'a accessible' do + let(:actions) { ['push'] } + end + end + + shared_examples 'a pullable and pushable' do + it_behaves_like 'a accessible' do + let(:actions) { ['pull', 'push'] } + end + end + + shared_examples 'a forbidden' do + it { is_expected.to include(http_status: 403) } + it { is_expected.not_to include(:token) } + end + + describe '#full_access_token' do + let(:project) { create(:empty_project) } + let(:token) { described_class.full_access_token(project.path_with_namespace) } + + subject { { token: token } } + + it_behaves_like 'a accessible' do + let(:actions) { ['*'] } + end + end + + context 'user authorization' do + let(:project) { create(:project) } + let(:current_user) { create(:user) } + + context 'allow to use scope-less authentication' do + it_behaves_like 'a valid token' + end + + context 'allow developer to push images' do + before { project.team << [current_user, :developer] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:push" } + end + + it_behaves_like 'a pushable' + end + + context 'allow reporter to pull images' do + before { project.team << [current_user, :reporter] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull" } + end + + it_behaves_like 'a pullable' + end + + context 'return a least of privileges' do + before { project.team << [current_user, :reporter] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:push,pull" } + end + + it_behaves_like 'a pullable' + end + + context 'disallow guest to pull or push images' do + before { project.team << [current_user, :guest] } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull,push" } + end + + it_behaves_like 'an inaccessible' + end + end + + context 'project authorization' do + let(:current_project) { create(:empty_project) } + + context 'allow to use scope-less authentication' do + it_behaves_like 'a valid token' + end + + context 'allow to pull and push images' do + let(:current_params) do + { scope: "repository:#{current_project.path_with_namespace}:pull,push" } + end + + it_behaves_like 'a pullable and pushable' do + let(:project) { current_project } + end + end + + context 'for other projects' do + context 'when pulling' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull" } + end + + context 'allow for public' do + let(:project) { create(:empty_project, :public) } + it_behaves_like 'a pullable' + end + + context 'disallow for private' do + let(:project) { create(:empty_project, :private) } + it_behaves_like 'an inaccessible' + end + end + + context 'when pushing' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:push" } + end + + context 'disallow for all' do + let(:project) { create(:empty_project, :public) } + it_behaves_like 'an inaccessible' + end + end + end + + context 'for project without container registry' do + let(:project) { create(:empty_project, :public, container_registry_enabled: false) } + + before { project.update(container_registry_enabled: false) } + + context 'disallow when pulling' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull" } + end + + it_behaves_like 'an inaccessible' + end + end + end + + context 'unauthorized' do + context 'disallow to use scope-less authentication' do + it_behaves_like 'a forbidden' + end + + context 'for invalid scope' do + let(:current_params) do + { scope: 'invalid:aa:bb' } + end + + it_behaves_like 'a forbidden' + end + + context 'for private project' do + let(:project) { create(:empty_project, :private) } + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull" } + end + + it_behaves_like 'a forbidden' + end + + context 'for public project' do + let(:project) { create(:empty_project, :public) } + + context 'when pulling and pushing' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull,push" } + end + + it_behaves_like 'a pullable' + end + + context 'when pushing' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:push" } + end + + it_behaves_like 'a forbidden' + end + end + end +end diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb index ecc3a88a262..984b78487d4 100644 --- a/spec/services/ci/create_builds_service_spec.rb +++ b/spec/services/ci/create_builds_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::CreateBuildsService, services: true do - let(:commit) { create(:ci_commit, ref: 'master') } + let(:pipeline) { create(:ci_pipeline, ref: 'master') } let(:user) { create(:user) } describe '#execute' do @@ -9,7 +9,7 @@ describe Ci::CreateBuildsService, services: true do # subject do - described_class.new(commit).execute(commit, nil, user, status) + described_class.new(pipeline).execute('test', nil, user, status) end context 'next builds available' do diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index dbdc5370bd8..ae4b7aca820 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -6,7 +6,7 @@ describe Ci::CreateTriggerRequestService, services: true do let(:trigger) { create(:ci_trigger, project: project) } before do - stub_ci_commit_to_return_yaml_file + stub_ci_pipeline_to_return_yaml_file end describe :execute do @@ -27,8 +27,8 @@ describe Ci::CreateTriggerRequestService, services: true do subject { service.execute(project, trigger, 'master') } before do - stub_ci_commit_yaml_file('{}') - FactoryGirl.create :ci_commit, project: project + stub_ci_pipeline_yaml_file('{}') + FactoryGirl.create :ci_pipeline, project: project end it { expect(subject).to be_nil } diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb index 4cc4b3870d1..476a888e394 100644 --- a/spec/services/ci/image_for_build_service_spec.rb +++ b/spec/services/ci/image_for_build_service_spec.rb @@ -5,8 +5,8 @@ module Ci let(:service) { ImageForBuildService.new } let(:project) { FactoryGirl.create(:empty_project) } let(:commit_sha) { '01234567890123456789' } - let(:commit) { project.ensure_ci_commit(commit_sha, 'master') } - let(:build) { FactoryGirl.create(:ci_build, commit: commit) } + let(:commit) { project.ensure_pipeline(commit_sha, 'master') } + let(:build) { FactoryGirl.create(:ci_build, pipeline: commit) } describe :execute do before { build } diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index e81f9e757ac..d91fc574299 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -4,8 +4,8 @@ module Ci describe RegisterBuildService, services: true do let!(:service) { RegisterBuildService.new } let!(:project) { FactoryGirl.create :empty_project, shared_runners_enabled: false } - let!(:commit) { FactoryGirl.create :ci_commit, project: project } - let!(:pending_build) { FactoryGirl.create :ci_build, commit: commit } + let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } + let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: pipeline } let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) } let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) } diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb index ea5dcfa068a..a5b4d9f05de 100644 --- a/spec/services/create_commit_builds_service_spec.rb +++ b/spec/services/create_commit_builds_service_spec.rb @@ -6,12 +6,12 @@ describe CreateCommitBuildsService, services: true do let(:user) { nil } before do - stub_ci_commit_to_return_yaml_file + stub_ci_pipeline_to_return_yaml_file end describe :execute do context 'valid params' do - let(:commit) do + let(:pipeline) do service.execute(project, user, ref: 'refs/heads/master', before: '00000000', @@ -20,11 +20,11 @@ describe CreateCommitBuildsService, services: true do ) end - it { expect(commit).to be_kind_of(Ci::Commit) } - it { expect(commit).to be_valid } - it { expect(commit).to be_persisted } - it { expect(commit).to eq(project.ci_commits.last) } - it { expect(commit.builds.first).to be_kind_of(Ci::Build) } + it { expect(pipeline).to be_kind_of(Ci::Pipeline) } + it { expect(pipeline).to be_valid } + it { expect(pipeline).to be_persisted } + it { expect(pipeline).to eq(project.pipelines.last) } + it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) } end context "skip tag if there is no build for it" do @@ -40,7 +40,7 @@ describe CreateCommitBuildsService, services: true do it "creates commit if there is no appropriate job but deploy job has right ref setting" do config = YAML.dump({ deploy: { deploy: "ls", only: ["0_1"] } }) - stub_ci_commit_yaml_file(config) + stub_ci_pipeline_yaml_file(config) result = service.execute(project, user, ref: 'refs/heads/0_1', @@ -52,8 +52,8 @@ describe CreateCommitBuildsService, services: true do end end - it 'skips creating ci_commit for refs without .gitlab-ci.yml' do - stub_ci_commit_yaml_file(nil) + it 'skips creating pipeline for refs without .gitlab-ci.yml' do + stub_ci_pipeline_yaml_file(nil) result = service.execute(project, user, ref: 'refs/heads/0_1', before: '00000000', @@ -61,115 +61,115 @@ describe CreateCommitBuildsService, services: true do commits: [{ message: 'Message' }] ) expect(result).to be_falsey - expect(Ci::Commit.count).to eq(0) + expect(Ci::Pipeline.count).to eq(0) end it 'fails commits if yaml is invalid' do message = 'message' - allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message } - stub_ci_commit_yaml_file('invalid: file: file') + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } + stub_ci_pipeline_yaml_file('invalid: file: file') commits = [{ message: message }] - commit = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(commit).to be_persisted - expect(commit.builds.any?).to be false - expect(commit.status).to eq('failed') - expect(commit.yaml_errors).to_not be_nil + pipeline = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq('failed') + expect(pipeline.yaml_errors).not_to be_nil end describe :ci_skip? do let(:message) { "some message[ci skip]" } before do - allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message } + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message } end it "skips builds creation if there is [ci skip] tag in commit message" do commits = [{ message: message }] - commit = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(commit).to be_persisted - expect(commit.builds.any?).to be false - expect(commit.status).to eq("skipped") + pipeline = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") end it "does not skips builds creation if there is no [ci skip] tag in commit message" do - allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { "some message" } + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" } commits = [{ message: "some message" }] - commit = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - - expect(commit).to be_persisted - expect(commit.builds.first.name).to eq("staging") + pipeline = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + + expect(pipeline).to be_persisted + expect(pipeline.builds.first.name).to eq("staging") end it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do - stub_ci_commit_yaml_file('invalid: file: fiile') + stub_ci_pipeline_yaml_file('invalid: file: fiile') commits = [{ message: message }] - commit = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(commit).to be_persisted - expect(commit.builds.any?).to be false - expect(commit.status).to eq("skipped") - expect(commit.yaml_errors).to be_nil + pipeline = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + expect(pipeline).to be_persisted + expect(pipeline.builds.any?).to be false + expect(pipeline.status).to eq("skipped") + expect(pipeline.yaml_errors).to be_nil end end it "skips build creation if there are already builds" do - allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { gitlab_ci_yaml } + allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { gitlab_ci_yaml } commits = [{ message: "message" }] - commit = service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(commit).to be_persisted - expect(commit.builds.count(:all)).to eq(2) + pipeline = service.execute(project, user, + ref: 'refs/heads/master', + before: '00000000', + after: '31das312', + commits: commits + ) + expect(pipeline).to be_persisted + expect(pipeline.builds.count(:all)).to eq(2) - commit = service.execute(project, user, - ref: 'refs/heads/master', - before: '00000000', - after: '31das312', - commits: commits - ) - expect(commit).to be_persisted - expect(commit.builds.count(:all)).to eq(2) + pipeline = service.execute(project, user, + ref: 'refs/heads/master', + before: '00000000', + after: '31das312', + commits: commits + ) + expect(pipeline).to be_persisted + expect(pipeline.builds.count(:all)).to eq(2) end it "creates commit with failed status if yaml is invalid" do - stub_ci_commit_yaml_file('invalid: file') + stub_ci_pipeline_yaml_file('invalid: file') commits = [{ message: "some message" }] - commit = service.execute(project, user, - ref: 'refs/tags/0_1', - before: '00000000', - after: '31das312', - commits: commits - ) + pipeline = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) - expect(commit).to be_persisted - expect(commit.status).to eq("failed") - expect(commit.builds.any?).to be false + expect(pipeline).to be_persisted + expect(pipeline.status).to eq("failed") + expect(pipeline.builds.any?).to be false end end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index eeab540c2fd..18692f1279a 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -158,49 +158,6 @@ describe GitPushService, services: true do end end - describe "Updates main language" do - context "before push" do - it { expect(project.main_language).to eq(nil) } - end - - context "after push" do - def execute - execute_service(project, user, @oldrev, @newrev, ref) - end - - context "to master" do - let(:ref) { @ref } - - context 'when main_language is nil' do - it 'obtains the language from the repository' do - expect(project.repository).to receive(:main_language) - execute - end - - it 'sets the project main language' do - execute - expect(project.main_language).to eq("Ruby") - end - end - - context 'when main_language is already set' do - it 'does not check the repository' do - execute # do an initial run to simulate lang being preset - expect(project.repository).not_to receive(:main_language) - execute - end - end - end - - context "to other branch" do - let(:ref) { 'refs/heads/feature/branch' } - - it { expect(project.main_language).to eq(nil) } - end - end - end - - describe "Updates git attributes" do context "for default branch" do it "calls the copy attributes method for the first push to the default branch" do diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index 6aefb48a4e8..71a0b8e2a12 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -13,8 +13,8 @@ describe Groups::CreateService, services: true do end context "cannot create group with restricted visibility level" do - before { allow(current_application_settings).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) } - it { is_expected.to_not be_persisted } + before { allow_any_instance_of(ApplicationSetting).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) } + it { is_expected.not_to be_persisted } end end end diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index e91906d0d49..4a689e64dc5 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -1,119 +1,265 @@ require 'spec_helper' describe Issues::BulkUpdateService, services: true do - let(:issue) { create(:issue, project: @project) } - - before do - @user = create :user - opts = { - name: "GitLab", - namespace: @user.namespace - } - @project = Projects::CreateService.new(@user, opts).execute - end + let(:user) { create(:user) } + let(:project) { Projects::CreateService.new(user, namespace: user.namespace, name: 'test').execute } - describe :close_issue do + let!(:result) { Issues::BulkUpdateService.new(project, user, params).execute } - before do - @issues = 5.times.collect do - create(:issue, project: @project) - end - @params = { + describe :close_issue do + let(:issues) { create_list(:issue, 5, project: project) } + let(:params) do + { state_event: 'close', - issues_ids: @issues.map(&:id) + issues_ids: issues.map(&:id).join(',') } end - it do - result = Issues::BulkUpdateService.new(@project, @user, @params).execute + it 'succeeds and returns the correct number of issues updated' do expect(result[:success]).to be_truthy - expect(result[:count]).to eq(@issues.count) - - expect(@project.issues.opened).to be_empty - expect(@project.issues.closed).not_to be_empty + expect(result[:count]).to eq(issues.count) end + it 'closes all the issues passed' do + expect(project.issues.opened).to be_empty + expect(project.issues.closed).not_to be_empty + end end describe :reopen_issues do - - before do - @issues = 5.times.collect do - create(:closed_issue, project: @project) - end - @params = { + let(:issues) { create_list(:closed_issue, 5, project: project) } + let(:params) do + { state_event: 'reopen', - issues_ids: @issues.map(&:id) + issues_ids: issues.map(&:id).join(',') } end - it do - result = Issues::BulkUpdateService.new(@project, @user, @params).execute + it 'succeeds and returns the correct number of issues updated' do expect(result[:success]).to be_truthy - expect(result[:count]).to eq(@issues.count) - - expect(@project.issues.closed).to be_empty - expect(@project.issues.opened).not_to be_empty + expect(result[:count]).to eq(issues.count) end + it 'reopens all the issues passed' do + expect(project.issues.closed).to be_empty + expect(project.issues.opened).not_to be_empty + end end - describe :update_assignee do + describe 'updating assignee' do + let(:issue) do + create(:issue, project: project) { |issue| issue.update_attributes(assignee: user) } + end - before do - @new_assignee = create :user - @params = { - issues_ids: [issue.id], - assignee_id: @new_assignee.id + let(:params) do + { + assignee_id: assignee_id, + issues_ids: issue.id.to_s } end - it do - result = Issues::BulkUpdateService.new(@project, @user, @params).execute - expect(result[:success]).to be_truthy - expect(result[:count]).to eq(1) + context 'when the new assignee ID is a valid user' do + let(:new_assignee) { create(:user) } + let(:assignee_id) { new_assignee.id } - expect(@project.issues.first.assignee).to eq(@new_assignee) - end + it 'succeeds' do + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(1) + end - it 'allows mass-unassigning' do - @project.issues.first.update_attribute(:assignee, @new_assignee) - expect(@project.issues.first.assignee).not_to be_nil + it 'updates the assignee to the use ID passed' do + expect(issue.reload.assignee).to eq(new_assignee) + end + end - @params[:assignee_id] = -1 + context 'when the new assignee ID is -1' do + let(:assignee_id) { -1 } - Issues::BulkUpdateService.new(@project, @user, @params).execute - expect(@project.issues.first.assignee).to be_nil + it 'unassigns the issues' do + expect(issue.reload.assignee).to be_nil + end end - it 'does not unassign when assignee_id is not present' do - @project.issues.first.update_attribute(:assignee, @new_assignee) - expect(@project.issues.first.assignee).not_to be_nil - - @params[:assignee_id] = '' + context 'when the new assignee ID is not present' do + let(:assignee_id) { nil } - Issues::BulkUpdateService.new(@project, @user, @params).execute - expect(@project.issues.first.assignee).not_to be_nil + it 'does not unassign' do + expect(issue.reload.assignee).to eq(user) + end end end - describe :update_milestone do + describe 'updating milestones' do + let(:issue) { create(:issue, project: project) } + let(:milestone) { create(:milestone, project: project) } - before do - @milestone = create(:milestone, project: @project) - @params = { - issues_ids: [issue.id], - milestone_id: @milestone.id + let(:params) do + { + issues_ids: issue.id.to_s, + milestone_id: milestone.id } end - it do - result = Issues::BulkUpdateService.new(@project, @user, @params).execute + it 'succeeds' do expect(result[:success]).to be_truthy expect(result[:count]).to eq(1) + end - expect(@project.issues.first.milestone).to eq(@milestone) + it 'updates the issue milestone' do + expect(project.issues.first.milestone).to eq(milestone) end end + describe 'updating labels' do + def create_issue_with_labels(labels) + create(:issue, project: project) { |issue| issue.update_attributes(labels: labels) } + end + + let(:bug) { create(:label, project: project) } + let(:regression) { create(:label, project: project) } + let(:merge_requests) { create(:label, project: project) } + + let(:issue_all_labels) { create_issue_with_labels([bug, regression, merge_requests]) } + let(:issue_bug_and_regression) { create_issue_with_labels([bug, regression]) } + let(:issue_bug_and_merge_requests) { create_issue_with_labels([bug, merge_requests]) } + let(:issue_no_labels) { create(:issue, project: project) } + let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests, issue_no_labels] } + + let(:labels) { [] } + let(:add_labels) { [] } + let(:remove_labels) { [] } + + let(:params) do + { + label_ids: labels.map(&:id), + add_label_ids: add_labels.map(&:id), + remove_label_ids: remove_labels.map(&:id), + issues_ids: issues.map(&:id).join(',') + } + end + + context 'when label_ids are passed' do + let(:issues) { [issue_all_labels, issue_no_labels] } + let(:labels) { [bug, regression] } + + it 'updates the labels of all issues passed to the labels passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(eq(labels.map(&:id))) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end + + context 'when those label IDs are empty' do + let(:labels) { [] } + + it 'updates the issues passed to have no labels' do + expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) + end + end + end + + context 'when add_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:add_labels) { [bug, regression, merge_requests] } + + it 'adds those label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(*add_labels.map(&:id))) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end + end + + context 'when remove_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:remove_labels) { [bug, regression, merge_requests] } + + it 'removes those label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end + end + + context 'when add_label_ids and remove_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:add_labels) { [bug] } + let(:remove_labels) { [merge_requests] } + + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + end + + it 'removes the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end + end + + context 'when add_label_ids and label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests] } + let(:labels) { [merge_requests] } + let(:add_labels) { [regression] } + + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(regression.id)) + end + + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + end + + it 'does not update issues not passed in' do + expect(issue_no_labels.label_ids).to be_empty + end + end + + context 'when remove_label_ids and label_ids are passed' do + let(:issues) { [issue_no_labels, issue_bug_and_regression] } + let(:labels) { [merge_requests] } + let(:remove_labels) { [regression] } + + it 'remove the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) + end + + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + end + + it 'does not update issues not passed in' do + expect(issue_all_labels.label_ids).to contain_exactly(bug.id, regression.id, merge_requests.id) + end + end + + context 'when add_label_ids, remove_label_ids, and label_ids are passed' do + let(:issues) { [issue_bug_and_merge_requests, issue_no_labels] } + let(:labels) { [regression] } + let(:add_labels) { [bug] } + let(:remove_labels) { [merge_requests] } + + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + end + + it 'removes the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + end + + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end + end + end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index ac28b6f71f9..1ee9f3aae4d 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -54,8 +54,8 @@ describe Issues::CreateService, services: true do label_ids: [label.id] } end - it 'does not assign label'do - expect(issue.labels).to_not include label + it 'does not assign label' do + expect(issue.labels).not_to include label end end @@ -69,7 +69,7 @@ describe Issues::CreateService, services: true do end it 'does not assign milestone' do - expect(issue.milestone).to_not eq milestone + expect(issue.milestone).not_to eq milestone end end end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index c15e26189a5..93bf0f64963 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -39,6 +39,7 @@ describe Issues::MoveService, services: true do let!(:milestone2) do create(:milestone, project_id: new_project.id, title: 'v9.0') end + let!(:award_emoji) { create(:award_emoji, awardable: old_issue) } let!(:new_issue) { move_service.execute(old_issue, new_project) } end @@ -115,6 +116,10 @@ describe Issues::MoveService, services: true do it 'preserves create time' do expect(old_issue.created_at).to eq new_issue.created_at end + + it 'moves the award emoji' do + expect(old_issue.award_emoji.first.name).to eq new_issue.reload.award_emoji.first.name + end end context 'issue with notes' do @@ -194,10 +199,10 @@ describe Issues::MoveService, services: true do include_context 'issue move executed' it 'rewrites uploads in description' do - expect(new_issue.description).to_not eq description + expect(new_issue.description).not_to eq description expect(new_issue.description) .to match(/Text and #{FileUploader::MARKDOWN_PATTERN}/) - expect(new_issue.description).to_not include uploader.secret + expect(new_issue.description).not_to include uploader.secret end end end @@ -231,7 +236,7 @@ describe Issues::MoveService, services: true do context 'user is reporter in both projects' do include_context 'user can move issue' - it { expect { move }.to_not raise_error } + it { expect { move }.not_to raise_error } end context 'user is reporter only in new project' do diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 52f69306994..dacbcd8fb46 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require 'spec_helper' describe Issues::UpdateService, services: true do @@ -27,11 +28,6 @@ describe Issues::UpdateService, services: true do end end - def update_issue(opts) - @issue = Issues::UpdateService.new(project, user, opts).execute(issue) - @issue.reload - end - context "valid params" do before do opts = { @@ -39,7 +35,8 @@ describe Issues::UpdateService, services: true do description: 'Also please fix', assignee_id: user2.id, state_event: 'close', - label_ids: [label.id] + label_ids: [label.id], + confidential: true } perform_enqueued_jobs do @@ -79,13 +76,25 @@ describe Issues::UpdateService, services: true do end it 'creates system note about title change' do - note = find_note('Title changed') + note = find_note('Changed title:') + + expect(note).not_to be_nil + expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**' + end + + it 'creates system note about confidentiality change' do + note = find_note('Made the issue confidential') expect(note).not_to be_nil - expect(note.note).to eq 'Title changed from **Old title** to **New title**' + expect(note.note).to eq 'Made the issue confidential' end end + def update_issue(opts) + @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + @issue.reload + end + context 'todos' do let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } @@ -265,5 +274,50 @@ describe Issues::UpdateService, services: true do end end end + + context 'updating labels' do + let(:label3) { create(:label, project: project) } + let(:result) { Issues::UpdateService.new(project, user, params).execute(issue).reload } + + context 'when add_label_ids and label_ids are passed' do + let(:params) { { label_ids: [label.id], add_label_ids: [label3.id] } } + + it 'ignores the label_ids parameter' do + expect(result.label_ids).not_to include(label.id) + end + + it 'adds the passed labels' do + expect(result.label_ids).to include(label3.id) + end + end + + context 'when remove_label_ids and label_ids are passed' do + let(:params) { { label_ids: [], remove_label_ids: [label.id] } } + + before { issue.update_attributes(labels: [label, label3]) } + + it 'ignores the label_ids parameter' do + expect(result.label_ids).not_to be_empty + end + + it 'removes the passed labels' do + expect(result.label_ids).not_to include(label.id) + end + end + + context 'when add_label_ids and remove_label_ids are passed' do + let(:params) { { add_label_ids: [label3.id], remove_label_ids: [label.id] } } + + before { issue.update_attributes(labels: [label]) } + + it 'adds the passed labels' do + expect(result.label_ids).to include(label3.id) + end + + it 'removes the passed labels' do + expect(result.label_ids).not_to include(label.id) + end + end + end end end diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb new file mode 100644 index 00000000000..dd656c3bbb7 --- /dev/null +++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +# Write specs in this file. +describe MergeRequests::AddTodoWhenBuildFailsService do + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request) } + let(:project) { create(:project) } + let(:sha) { '1234567890abcdef1234567890abcdef12345678' } + let(:pipeline) { create(:ci_pipeline_with_one_job, ref: merge_request.source_branch, project: project, sha: sha) } + let(:service) { MergeRequests::AddTodoWhenBuildFailsService.new(project, user, commit_message: 'Awesome message') } + let(:todo_service) { TodoService.new } + + let(:merge_request) do + create(:merge_request, merge_user: user, source_branch: 'master', + target_branch: 'feature', source_project: project, target_project: project, + state: 'opened') + end + + before do + allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline) + allow(service).to receive(:todo_service).and_return(todo_service) + end + + describe '#execute' do + context 'commit status with ref' do + let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, pipeline: pipeline) } + + it 'notifies the todo service' do + expect(todo_service).to receive(:merge_request_build_failed).with(merge_request) + service.execute(commit_status) + end + end + + context 'commit status with non-HEAD ref' do + let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch) } + + it 'does not notify the todo service' do + expect(todo_service).not_to receive(:merge_request_build_failed) + service.execute(commit_status) + end + end + + context 'commit status without ref' do + let(:commit_status) { create(:generic_commit_status) } + + it 'does not notify the todo service' do + expect(todo_service).not_to receive(:merge_request_build_failed) + service.execute(commit_status) + end + end + end + + describe '#close' do + context 'commit status with ref' do + let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, pipeline: pipeline) } + + it 'notifies the todo service' do + expect(todo_service).to receive(:merge_request_build_retried).with(merge_request) + service.close(commit_status) + end + end + + context 'commit status with non-HEAD ref' do + let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch) } + + it 'does not notify the todo service' do + expect(todo_service).not_to receive(:merge_request_build_retried) + service.close(commit_status) + end + end + + context 'commit status without ref' do + let(:commit_status) { create(:generic_commit_status) } + + it 'does not notify the todo service' do + expect(todo_service).not_to receive(:merge_request_build_retried) + service.close(commit_status) + end + end + end +end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 120f4d6a669..e433f49872d 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -12,7 +12,8 @@ describe MergeRequests::CreateService, services: true do title: 'Awesome merge_request', description: 'please fix', source_branch: 'feature', - target_branch: 'master' + target_branch: 'master', + force_remove_source_branch: '1' } end @@ -29,6 +30,7 @@ describe MergeRequests::CreateService, services: true do it { expect(@merge_request).to be_valid } it { expect(@merge_request.title).to eq('Awesome merge_request') } it { expect(@merge_request.assignee).to be_nil } + it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') } it 'should execute hooks with default action' do expect(service).to have_received(:execute_hooks).with(@merge_request) diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index ceb3f97280e..1b0396eb686 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -38,6 +38,21 @@ describe MergeRequests::MergeService, services: true do end end + context 'remove source branch by author' do + let(:service) do + merge_request.merge_params['force_remove_source_branch'] = '1' + merge_request.save! + MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') + end + + it 'removes the source branch' do + expect(DeleteBranchService).to receive(:new). + with(merge_request.source_project, merge_request.author). + and_call_original + service.execute(merge_request) + end + end + context "error handling" do let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') } diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index 52a302e0e1a..4da8146e3d6 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' describe MergeRequests::MergeWhenBuildSucceedsService do - let(:user) { create(:user) } - let(:merge_request) { create(:merge_request) } + let(:user) { create(:user) } + let(:project) { create(:project) } let(:mr_merge_if_green_enabled) do create(:merge_request, merge_when_build_succeeds: true, merge_user: user, @@ -10,14 +10,18 @@ describe MergeRequests::MergeWhenBuildSucceedsService do source_project: project, target_project: project, state: "opened") end - let(:project) { create(:project) } - let(:ci_commit) { create(:ci_commit_with_one_job, ref: mr_merge_if_green_enabled.source_branch, project: project) } + let(:pipeline) { create(:ci_pipeline_with_one_job, ref: mr_merge_if_green_enabled.source_branch, project: project) } let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, commit_message: 'Awesome message') } describe "#execute" do + let(:merge_request) do + create(:merge_request, target_project: project, source_project: project, + source_branch: "feature", target_branch: 'master') + end + context 'first time enabling' do before do - allow(merge_request).to receive(:ci_commit).and_return(ci_commit) + allow(merge_request).to receive(:pipeline).and_return(pipeline) service.execute(merge_request) end @@ -39,9 +43,9 @@ describe MergeRequests::MergeWhenBuildSucceedsService do let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) } before do - allow(mr_merge_if_green_enabled).to receive(:ci_commit).and_return(ci_commit) + allow(mr_merge_if_green_enabled).to receive(:pipeline).and_return(pipeline) allow(mr_merge_if_green_enabled).to receive(:mergeable?).and_return(true) - allow(ci_commit).to receive(:success?).and_return(true) + allow(pipeline).to receive(:success?).and_return(true) end it 'updates the merge params' do @@ -58,8 +62,8 @@ describe MergeRequests::MergeWhenBuildSucceedsService do let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") } it "merges all merge requests with merge when build succeeds enabled" do - allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) - allow(ci_commit).to receive(:success?).and_return(true) + allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline) + allow(pipeline).to receive(:success?).and_return(true) expect(MergeWorker).to receive(:perform_async) service.trigger(build) @@ -71,11 +75,11 @@ describe MergeRequests::MergeWhenBuildSucceedsService do let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") } it "merges all merge requests with merge when build succeeds enabled" do - allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) - allow(ci_commit).to receive(:success?).and_return(true) + allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline) + allow(pipeline).to receive(:success?).and_return(true) allow(old_build).to receive(:sha).and_return('1234abcdef') - expect(MergeWorker).to_not receive(:perform_async) + expect(MergeWorker).not_to receive(:perform_async) service.trigger(old_build) end end @@ -88,16 +92,16 @@ describe MergeRequests::MergeWhenBuildSucceedsService do it "doesn't merge a requests for status on other branch" do allow(project.repository).to receive(:branch_names_contains).with(commit_status.sha).and_return([]) - expect(MergeWorker).to_not receive(:perform_async) + expect(MergeWorker).not_to receive(:perform_async) service.trigger(commit_status) end it 'discovers branches and merges all merge requests when status is success' do allow(project.repository).to receive(:branch_names_contains). with(commit_status.sha).and_return([mr_merge_if_green_enabled.source_branch]) - allow(ci_commit).to receive(:success?).and_return(true) - allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) - allow(ci_commit).to receive(:success?).and_return(true) + allow(pipeline).to receive(:success?).and_return(true) + allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline) + allow(pipeline).to receive(:success?).and_return(true) expect(MergeWorker).to receive(:perform_async) service.trigger(commit_status) @@ -106,23 +110,23 @@ describe MergeRequests::MergeWhenBuildSucceedsService do context 'properly handles multiple stages' do let(:ref) { mr_merge_if_green_enabled.source_branch } - let(:build) { create(:ci_build, commit: ci_commit, ref: ref, name: 'build', stage: 'build') } - let(:test) { create(:ci_build, commit: ci_commit, ref: ref, name: 'test', stage: 'test') } + let(:build) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') } + let(:test) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') } before do # This behavior of MergeRequest: we instantiate a new object - allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_wrap_original do - Ci::Commit.find(ci_commit.id) + allow_any_instance_of(MergeRequest).to receive(:pipeline).and_wrap_original do + Ci::Pipeline.find(pipeline.id) end # We create test after the build - allow(ci_commit).to receive(:create_next_builds).and_wrap_original do + allow(pipeline).to receive(:create_next_builds).and_wrap_original do test end end it "doesn't merge if some stages failed" do - expect(MergeWorker).to_not receive(:perform_async) + expect(MergeWorker).not_to receive(:perform_async) build.success test.drop end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index fea8182bd30..31b93850c7c 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -27,6 +27,20 @@ describe MergeRequests::RefreshService, services: true do target_branch: 'feature', target_project: @project) + @build_failed_todo = create(:todo, + :build_failed, + user: @user, + project: @project, + target: @merge_request, + author: @user) + + @fork_build_failed_todo = create(:todo, + :build_failed, + user: @user, + project: @project, + target: @merge_request, + author: @user) + @commits = @merge_request.commits @oldrev = @commits.last.id @@ -51,6 +65,8 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request.merge_when_build_succeeds).to be_falsey} it { expect(@fork_merge_request).to be_open } it { expect(@fork_merge_request.notes).to be_empty } + it { expect(@build_failed_todo).to be_done } + it { expect(@fork_build_failed_todo).to be_done } end context 'push to origin repo target branch' do @@ -63,6 +79,8 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request).to be_merged } it { expect(@fork_merge_request).to be_merged } it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') } + it { expect(@build_failed_todo).to be_pending } + it { expect(@fork_build_failed_todo).to be_pending } end context 'manual merge of source branch' do @@ -82,6 +100,8 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request.diffs.size).to be > 0 } it { expect(@fork_merge_request).to be_merged } it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') } + it { expect(@build_failed_todo).to be_pending } + it { expect(@fork_build_failed_todo).to be_pending } end context 'push to fork repo source branch' do @@ -101,6 +121,8 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request).to be_open } it { expect(@fork_merge_request.notes.last.note).to include('Added 4 commits') } it { expect(@fork_merge_request).to be_open } + it { expect(@build_failed_todo).to be_pending } + it { expect(@fork_build_failed_todo).to be_pending } end context 'push to fork repo target branch' do @@ -113,6 +135,8 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request).to be_open } it { expect(@fork_merge_request.notes).to be_empty } it { expect(@fork_merge_request).to be_open } + it { expect(@build_failed_todo).to be_pending } + it { expect(@fork_build_failed_todo).to be_pending } end context 'push to origin repo target branch after fork project was removed' do @@ -126,6 +150,8 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request).to be_merged } it { expect(@fork_merge_request).to be_open } it { expect(@fork_merge_request.notes).to be_empty } + it { expect(@build_failed_todo).to be_pending } + it { expect(@fork_build_failed_todo).to be_pending } end context 'push new branch that exists in a merge request' do @@ -153,6 +179,8 @@ describe MergeRequests::RefreshService, services: true do def reload_mrs @merge_request.reload @fork_merge_request.reload + @build_failed_todo.reload + @fork_build_failed_todo.reload end end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 213e8c2eb3a..d4ebe28c276 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -39,7 +39,8 @@ describe MergeRequests::UpdateService, services: true do assignee_id: user2.id, state_event: 'close', label_ids: [label.id], - target_branch: 'target' + target_branch: 'target', + force_remove_source_branch: '1' } end @@ -61,6 +62,7 @@ describe MergeRequests::UpdateService, services: true do it { expect(@merge_request.labels.count).to eq(1) } it { expect(@merge_request.labels.first.title).to eq(label.name) } it { expect(@merge_request.target_branch).to eq('target') } + it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') } it 'should execute hooks with update action' do expect(service).to have_received(:execute_hooks). @@ -90,10 +92,10 @@ describe MergeRequests::UpdateService, services: true do end it 'creates system note about title change' do - note = find_note('Title changed') + note = find_note('Changed title:') expect(note).not_to be_nil - expect(note.note).to eq 'Title changed from **Old title** to **New title**' + expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**' end it 'creates system note about branch change' do diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index ff23f13e1cb..35f576874b8 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -14,7 +14,7 @@ describe Notes::CreateService, services: true do noteable_type: 'Issue', noteable_id: issue.id } - + @note = Notes::CreateService.new(project, user, opts).execute end @@ -28,18 +28,16 @@ describe Notes::CreateService, services: true do project.team << [user, :master] end - it "creates emoji note" do + it "creates an award emoji" do opts = { note: ':smile: ', noteable_type: 'Issue', noteable_id: issue.id } + note = Notes::CreateService.new(project, user, opts).execute - @note = Notes::CreateService.new(project, user, opts).execute - - expect(@note).to be_valid - expect(@note.note).to eq('smile') - expect(@note.is_award).to be_truthy + expect(note).to be_valid + expect(note.name).to eq('smile') end it "creates regular note if emoji name is invalid" do @@ -48,12 +46,22 @@ describe Notes::CreateService, services: true do noteable_type: 'Issue', noteable_id: issue.id } + note = Notes::CreateService.new(project, user, opts).execute + + expect(note).to be_valid + expect(note.note).to eq(opts[:note]) + end + + it "normalizes the emoji name" do + opts = { + note: ':+1:', + noteable_type: 'Issue', + noteable_id: issue.id + } - @note = Notes::CreateService.new(project, user, opts).execute + expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user) - expect(@note).to be_valid - expect(@note.note).to eq(opts[:note]) - expect(@note.is_award).to be_falsy + Notes::CreateService.new(project, user, opts).execute end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 4bbc4ddc3ab..cef5e0d8659 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -66,6 +66,7 @@ describe NotificationService, services: true do should_email(@subscriber) should_email(@watcher_and_subscriber) should_email(@subscribed_participant) + should_not_email(@u_guest_watcher) should_not_email(note.author) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -100,6 +101,7 @@ describe NotificationService, services: true do should_email(note.noteable.author) should_email(note.noteable.assignee) should_email(@u_mentioned) + should_not_email(@u_guest_watcher) should_not_email(@u_watcher) should_not_email(note.author) should_not_email(@u_participating) @@ -160,6 +162,7 @@ describe NotificationService, services: true do should_email(member) end + should_email(@u_guest_watcher) should_email(note.noteable.author) should_email(note.noteable.assignee) should_not_email(note.author) @@ -201,6 +204,7 @@ describe NotificationService, services: true do should_email(member) end + should_email(@u_guest_watcher) should_email(note.noteable.author) should_not_email(note.author) should_email(@u_mentioned) @@ -224,6 +228,7 @@ describe NotificationService, services: true do it do notification.new_note(note) + should_email(@u_guest_watcher) should_email(@u_committer) should_email(@u_watcher) should_not_email(@u_mentioned) @@ -236,6 +241,7 @@ describe NotificationService, services: true do note.update_attribute(:note, '@mention referenced') notification.new_note(note) + should_email(@u_guest_watcher) should_email(@u_committer) should_email(@u_watcher) should_email(@u_mentioned) @@ -269,6 +275,7 @@ describe NotificationService, services: true do should_email(issue.assignee) should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_not_email(@u_mentioned) should_not_email(@u_participating) @@ -328,6 +335,7 @@ describe NotificationService, services: true do should_email(issue.assignee) should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) should_not_email(@unsubscriber) @@ -342,6 +350,7 @@ describe NotificationService, services: true do should_email(@u_mentioned) should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) should_not_email(@unsubscriber) @@ -356,6 +365,7 @@ describe NotificationService, services: true do expect(issue.assignee).to be @u_mentioned should_email(issue.assignee) should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) should_not_email(@unsubscriber) @@ -370,6 +380,7 @@ describe NotificationService, services: true do expect(issue.assignee).to be @u_mentioned should_email(issue.assignee) should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) should_not_email(@unsubscriber) @@ -383,6 +394,7 @@ describe NotificationService, services: true do expect(issue.assignee).to be @u_mentioned should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) should_not_email(issue.assignee) @@ -411,6 +423,7 @@ describe NotificationService, services: true do should_not_email(issue.assignee) should_not_email(issue.author) should_not_email(@u_watcher) + should_not_email(@u_guest_watcher) should_not_email(@u_participant_mentioned) should_not_email(@subscriber) should_not_email(@watcher_and_subscriber) @@ -459,6 +472,7 @@ describe NotificationService, services: true do should_email(issue.assignee) should_email(issue.author) should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) @@ -475,6 +489,7 @@ describe NotificationService, services: true do should_email(issue.assignee) should_email(issue.author) should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) @@ -502,6 +517,7 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@watcher_and_subscriber) should_email(@u_participant_mentioned) + should_email(@u_guest_watcher) should_not_email(@u_participating) should_not_email(@u_disabled) end @@ -525,6 +541,7 @@ describe NotificationService, services: true do should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) + should_email(@u_guest_watcher) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -566,6 +583,7 @@ describe NotificationService, services: true do should_email(merge_request.assignee) should_email(@u_watcher) + should_email(@u_guest_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) @@ -584,6 +602,7 @@ describe NotificationService, services: true do should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) + should_email(@u_guest_watcher) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -599,6 +618,7 @@ describe NotificationService, services: true do should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) + should_email(@u_guest_watcher) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -620,6 +640,7 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@u_participating) + should_not_email(@u_guest_watcher) should_not_email(@u_disabled) end end @@ -635,6 +656,8 @@ describe NotificationService, services: true do @u_not_mentioned = create(:user, username: 'regular', notification_level: :participating) @u_outsider_mentioned = create(:user, username: 'outsider') + create_guest_watcher + project.team << [@u_watcher, :master] project.team << [@u_participating, :master] project.team << [@u_participant_mentioned, :master] @@ -644,6 +667,13 @@ describe NotificationService, services: true do project.team << [@u_not_mentioned, :master] end + def create_guest_watcher + @u_guest_watcher = create(:user, username: 'guest_watching') + setting = @u_guest_watcher.notification_settings_for(project) + setting.level = :watch + setting.save + end + def add_users_with_subscription(project, issuable) @subscriber = create :user @unsubscriber = create :user diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index e43903dbd3c..fd114359467 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -64,7 +64,7 @@ describe Projects::CreateService, services: true do @path = ProjectWiki.new(@project, @user).send(:path_to_repo) end - it { expect(File.exists?(@path)).to be_truthy } + it { expect(File.exist?(@path)).to be_truthy } end context 'wiki_enabled false does not create wiki repository directory' do @@ -74,7 +74,7 @@ describe Projects::CreateService, services: true do @path = ProjectWiki.new(@project, @user).send(:path_to_repo) end - it { expect(File.exists?(@path)).to be_falsey } + it { expect(File.exist?(@path)).to be_falsey } end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 1ec27077717..29341c5e57e 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -13,8 +13,8 @@ describe Projects::DestroyService, services: true do end it { expect(Project.all).not_to include(project) } - it { expect(Dir.exists?(path)).to be_falsey } - it { expect(Dir.exists?(remove_path)).to be_falsey } + it { expect(Dir.exist?(path)).to be_falsey } + it { expect(Dir.exist?(remove_path)).to be_falsey } end context 'Sidekiq fake' do @@ -24,8 +24,31 @@ describe Projects::DestroyService, services: true do end it { expect(Project.all).not_to include(project) } - it { expect(Dir.exists?(path)).to be_falsey } - it { expect(Dir.exists?(remove_path)).to be_truthy } + it { expect(Dir.exist?(path)).to be_falsey } + it { expect(Dir.exist?(remove_path)).to be_truthy } + end + + context 'container registry' do + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags('tag') + end + + context 'tags deletion succeeds' do + it do + expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(true) + + destroy_project(project, user, {}) + end + end + + context 'tags deletion fails' do + before { expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(false) } + + subject { destroy_project(project, user, {}) } + + it { expect{subject}.to raise_error(Projects::DestroyService::DestroyError) } + end end def destroy_project(project, user, params) diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index d1ee60a0aea..31bb7120d84 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -42,6 +42,33 @@ describe Projects::ForkService, services: true do expect(@to_project.builds_enabled?).to be_truthy end end + + context "when project has restricted visibility level" do + context "and only one visibility level is restricted" do + before do + @from_project.update_attributes(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) + end + + it "creates fork with highest allowed level" do + forked_project = fork_project(@from_project, @to_user) + + expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + end + + context "and all visibility levels are restricted" do + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE]) + end + + it "creates fork with private visibility levels" do + forked_project = fork_project(@from_project, @to_user) + + expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + end + end end describe :fork_to_namespace do diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 7f2dcdab960..068c9a1219c 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -49,7 +49,7 @@ describe Projects::ImportService, services: true do result = subject.execute expect(result[:status]).to eq :error - expect(result[:message]).to eq 'Failed to import the repository' + expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Failed to import the repository" end end @@ -124,7 +124,7 @@ describe Projects::ImportService, services: true do } ) - Gitlab.config.omniauth.providers << provider + allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider]) end end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 06017317339..d5aa115a074 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -26,6 +26,17 @@ describe Projects::TransferService, services: true do it { expect(project.namespace).to eq(user.namespace) } end + context 'disallow transfering of project with tags' do + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags('tag') + end + + subject { transfer_project(project, user, group) } + + it { is_expected.to be_falsey } + end + context 'namespace -> not allowed namespace' do before do @result = transfer_project(project, user, group) diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 5fbf2ae5247..09f0ee3871d 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -208,8 +208,10 @@ describe SystemNoteService, services: true do end describe '.merge_when_build_succeeds' do - let(:ci_commit) { build :ci_commit_without_jobs } - let(:noteable) { create :merge_request } + let(:pipeline) { build(:ci_pipeline_without_jobs )} + let(:noteable) do + create(:merge_request, source_project: project, target_project: project) + end subject { described_class.merge_when_build_succeeds(noteable, project, author, noteable.last_commit) } @@ -221,8 +223,9 @@ describe SystemNoteService, services: true do end describe '.cancel_merge_when_build_succeeds' do - let(:ci_commit) { build :ci_commit_without_jobs } - let(:noteable) { create :merge_request } + let(:noteable) do + create(:merge_request, source_project: project, target_project: project) + end subject { described_class.cancel_merge_when_build_succeeds(noteable, project, author) } @@ -241,15 +244,19 @@ describe SystemNoteService, services: true do it 'sets the note text' do expect(subject.note). - to eq "Title changed from **Old title** to **#{noteable.title}**" + to eq "Changed title: **{-Old title-}** → **{+#{noteable.title}+}**" end end + end - context 'when noteable does not respond to `title' do - let(:noteable) { double('noteable') } + describe '.change_issue_confidentiality' do + subject { described_class.change_issue_confidentiality(noteable, project, author) } - it 'returns nil' do - expect(subject).to be_nil + context 'when noteable responds to `confidential`' do + it_behaves_like 'a system note' + + it 'sets the note text' do + expect(subject.note).to eq 'Made the issue visible' end end end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 82b7fbfa816..489c920f19f 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -18,7 +18,7 @@ describe TodoService, services: true do end describe 'Issues' do - let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: mentions) } + let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } let(:unassigned_issue) { create(:issue, project: project, assignee: nil) } let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) } @@ -55,6 +55,25 @@ describe TodoService, services: true do should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) end + + context 'when a private group is mentioned' do + let(:group) { create :group, :private } + let(:project) { create :project, :private, group: group } + let(:issue) { create :issue, author: author, project: project, description: group.to_reference } + + before do + group.add_owner(author) + group.add_user(member, Gitlab::Access::DEVELOPER) + group.add_user(john_doe, Gitlab::Access::DEVELOPER) + + service.new_issue(issue, author) + end + + it 'creates a todo for group members' do + should_create_todo(user: member, target: issue) + should_create_todo(user: john_doe, target: issue) + end + end end describe '#update_issue' do @@ -82,6 +101,19 @@ describe TodoService, services: true do should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED) end + + it 'does not create todo when when tasks are marked as completed' do + issue.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") + + service.update_issue(issue, author) + + should_not_create_todo(user: admin, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: assignee, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: member, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED) + end end describe '#close_issue' do @@ -137,7 +169,6 @@ describe TodoService, services: true do let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) } let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) } let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) } - let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') } let(:system_note) { create(:system_note, project: project, noteable: issue) } it 'mark related pending todos to the noteable for the note author as done' do @@ -150,13 +181,6 @@ describe TodoService, services: true do expect(second_todo.reload).to be_done end - it 'mark related pending todos to the noteable for the award note author as done' do - service.new_note(award_note, john_doe) - - expect(first_todo.reload).to be_done - expect(second_todo.reload).to be_done - end - it 'does not mark related pending todos it is a system note' do service.new_note(system_note, john_doe) @@ -199,7 +223,7 @@ describe TodoService, services: true do end describe 'Merge Requests' do - let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: mentions) } + let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) } describe '#new_merge_request' do @@ -242,6 +266,19 @@ describe TodoService, services: true do expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count) end + + it 'does not create todo when when tasks are marked as completed' do + mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}") + + service.update_merge_request(mr_assigned, author) + + should_not_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: assignee, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED) + end end describe '#close_merge_request' do @@ -286,6 +323,34 @@ describe TodoService, services: true do expect(second_todo.reload).to be_done end end + + describe '#new_award_emoji' do + it 'marks related pending todos to the target for the user as done' do + todo = create(:todo, user: john_doe, project: project, target: mr_assigned, author: author) + service.new_award_emoji(mr_assigned, john_doe) + + expect(todo.reload).to be_done + end + end + + describe '#merge_request_build_failed' do + it 'creates a pending todo for the merge request author' do + service.merge_request_build_failed(mr_unassigned) + + should_create_todo(user: author, target: mr_unassigned, action: Todo::BUILD_FAILED) + end + end + + describe '#merge_request_push' do + it 'marks related pending todos to the target for the user as done' do + first_todo = create(:todo, :build_failed, user: author, project: project, target: mr_assigned, author: john_doe) + second_todo = create(:todo, :build_failed, user: john_doe, project: project, target: mr_assigned, author: john_doe) + service.merge_request_push(mr_assigned, author) + + expect(first_todo.reload).to be_done + expect(second_todo.reload).not_to be_done + end + end end def should_create_todo(attributes = {}) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 576d16e7ea3..b43f38ef202 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -16,6 +16,11 @@ require 'shoulda/matchers' require 'sidekiq/testing/inline' require 'rspec/retry' +if ENV['CI'] + require 'knapsack' + Knapsack::Adapters::RSpecAdapter.bind +end + # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } diff --git a/spec/support/fake_u2f_device.rb b/spec/support/fake_u2f_device.rb new file mode 100644 index 00000000000..553fe9f1fbc --- /dev/null +++ b/spec/support/fake_u2f_device.rb @@ -0,0 +1,36 @@ +class FakeU2fDevice + def initialize(page) + @page = page + end + + def respond_to_u2f_registration + app_id = @page.evaluate_script('gon.u2f.app_id') + challenges = @page.evaluate_script('gon.u2f.challenges') + + json_response = u2f_device(app_id).register_response(challenges[0]) + + @page.execute_script(" + u2f.register = function(appId, registerRequests, signRequests, callback) { + callback(#{json_response}); + }; + ") + end + + def respond_to_u2f_authentication + app_id = @page.evaluate_script('gon.u2f.app_id') + challenges = @page.evaluate_script('gon.u2f.challenges') + json_response = u2f_device(app_id).sign_response(challenges[0]) + + @page.execute_script(" + u2f.sign = function(appId, challenges, signRequests, callback) { + callback(#{json_response}); + }; + ") + end + + private + + def u2f_device(app_id) + @u2f_device ||= U2F::FakeU2F.new(app_id) + end +end diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb index e849a9633b9..a8e454eb09e 100644 --- a/spec/support/filter_spec_helper.rb +++ b/spec/support/filter_spec_helper.rb @@ -40,8 +40,7 @@ module FilterSpecHelper filters = [ Banzai::Filter::AutolinkFilter, - described_class, - Banzai::Filter::ReferenceGathererFilter + described_class ] HTML::Pipeline.new(filters, context) diff --git a/spec/controllers/import/import_spec_helper.rb b/spec/support/import_spec_helper.rb index 9d7648e25a7..6710962f082 100644 --- a/spec/controllers/import/import_spec_helper.rb +++ b/spec/support/import_spec_helper.rb @@ -28,6 +28,6 @@ module ImportSpecHelper app_id: 'asd123', app_secret: 'asd123' ) - Gitlab.config.omniauth.providers << provider + allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider]) end end diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb index a3f496359b1..5ebe095743b 100644 --- a/spec/support/jira_service_helper.rb +++ b/spec/support/jira_service_helper.rb @@ -2,11 +2,11 @@ module JiraServiceHelper def jira_service_settings properties = { - "title"=>"JIRA tracker", - "project_url"=>"http://jira.example/issues/?jql=project=A", - "issues_url"=>"http://jira.example/browse/JIRA-1", - "new_issue_url"=>"http://jira.example/secure/CreateIssue.jspa", - "api_url"=>"http://jira.example/rest/api/2" + "title" => "JIRA tracker", + "project_url" => "http://jira.example/issues/?jql=project=A", + "issues_url" => "http://jira.example/browse/JIRA-1", + "new_issue_url" => "http://jira.example/secure/CreateIssue.jspa", + "api_url" => "http://jira.example/rest/api/2" } jira_tracker.update_attributes(properties: properties, active: true) diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index cd9fdc6f18e..7a0f078c72b 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -26,11 +26,13 @@ module LoginHelpers # Internal: Login as the specified user # - # user - User instance to login with - def login_with(user) + # user - User instance to login with + # remember - Whether or not to check "Remember me" (default: false) + def login_with(user, remember: false) visit new_user_session_path fill_in "user_login", with: user.email fill_in "user_password", with: "12345678" + check 'user_remember_me' if remember click_button "Sign in" Thread.current[:current_user] = user end diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index b87cd6bbca2..a79386b5db9 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -32,6 +32,10 @@ class MarkdownFeature @project_wiki ||= ProjectWiki.new(project, user) end + def project_wiki_page + @project_wiki_page ||= build(:wiki_page, wiki: project_wiki) + end + def issue @issue ||= create(:issue, project: project) end @@ -63,8 +67,12 @@ class MarkdownFeature @label ||= create(:label, name: 'awaiting feedback', project: project) end + def simple_milestone + @simple_milestone ||= create(:milestone, name: 'gfm-milestone', project: project) + end + def milestone - @milestone ||= create(:milestone, project: project) + @milestone ||= create(:milestone, name: 'next goal', project: project) end # Cross-references ----------------------------------------------------------- diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 43cb6ef43f2..e005058ba5b 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -154,7 +154,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-milestone', count: 3) + expect(actual).to have_selector('a.gfm.gfm-milestone', count: 6) end end @@ -168,6 +168,16 @@ module MarkdownMatchers expect(actual).to have_selector('input[checked]', count: 3) end end + + # InlineDiffFilter + matcher :parse_inline_diffs do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('span.idiff.addition', count: 2) + expect(actual).to have_selector('span.idiff.deletion', count: 2) + end + end end # Monkeypatch the matcher DSL so that we can reduce some noisy duplication for diff --git a/spec/support/reference_parser_helpers.rb b/spec/support/reference_parser_helpers.rb new file mode 100644 index 00000000000..01689194eac --- /dev/null +++ b/spec/support/reference_parser_helpers.rb @@ -0,0 +1,5 @@ +module ReferenceParserHelpers + def empty_html_link + Nokogiri::HTML.fragment('<a></a>').children[0] + end +end diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index eec2e681117..93f96cacc00 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -13,18 +13,35 @@ module StubGitlabCalls allow_any_instance_of(Network).to receive(:projects) { project_hash_array } end - def stub_ci_commit_to_return_yaml_file - stub_ci_commit_yaml_file(gitlab_ci_yaml) + def stub_ci_pipeline_to_return_yaml_file + stub_ci_pipeline_yaml_file(gitlab_ci_yaml) end - def stub_ci_commit_yaml_file(ci_yaml) - allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { ci_yaml } + def stub_ci_pipeline_yaml_file(ci_yaml) + allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { ci_yaml } end def stub_ci_builds_disabled allow_any_instance_of(Project).to receive(:builds_enabled?).and_return(false) end + def stub_container_registry_config(registry_settings) + allow(Gitlab.config.registry).to receive_messages(registry_settings) + allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token') + end + + def stub_container_registry_tags(*tags) + allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_tags).and_return( + { "tags" => tags } + ) + allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_manifest).and_return( + JSON.load(File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json')) + ) + allow_any_instance_of(ContainerRegistry::Client).to receive(:blob).and_return( + File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json') + ) + end + private def gitlab_url @@ -36,20 +53,20 @@ module StubGitlabCalls stub_request(:post, "#{gitlab_url}api/v3/session.json"). with(body: "{\"email\":\"test@test.com\",\"password\":\"123456\"}", - headers: { 'Content-Type'=>'application/json' }). - to_return(status: 201, body: f, headers: { 'Content-Type'=>'application/json' }) + headers: { 'Content-Type' => 'application/json' }). + to_return(status: 201, body: f, headers: { 'Content-Type' => 'application/json' }) end def stub_user f = File.read(Rails.root.join('spec/support/gitlab_stubs/user.json')) stub_request(:get, "#{gitlab_url}api/v3/user?private_token=Wvjy2Krpb7y8xi93owUz"). - with(headers: { 'Content-Type'=>'application/json' }). - to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' }) + with(headers: { 'Content-Type' => 'application/json' }). + to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) stub_request(:get, "#{gitlab_url}api/v3/user?access_token=some_token"). - with(headers: { 'Content-Type'=>'application/json' }). - to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' }) + with(headers: { 'Content-Type' => 'application/json' }). + to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) end def stub_project_8 @@ -66,19 +83,19 @@ module StubGitlabCalls f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json')) stub_request(:get, "#{gitlab_url}api/v3/projects.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz"). - with(headers: { 'Content-Type'=>'application/json' }). - to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' }) + with(headers: { 'Content-Type' => 'application/json' }). + to_return(status: 200, body: f, headers: { 'Content-Type' => 'application/json' }) end def stub_projects_owned stub_request(:get, "#{gitlab_url}api/v3/projects/owned.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz"). - with(headers: { 'Content-Type'=>'application/json' }). + with(headers: { 'Content-Type' => 'application/json' }). to_return(status: 200, body: "", headers: {}) end def stub_ci_enable stub_request(:put, "#{gitlab_url}api/v3/projects/2/services/gitlab-ci.json?private_token=Wvjy2Krpb7y8xi93owUz"). - with(headers: { 'Content-Type'=>'application/json' }). + with(headers: { 'Content-Type' => 'application/json' }). to_return(status: 200, body: "", headers: {}) end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 05fc4c4554f..25da0917134 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' require 'rake' describe 'gitlab:app namespace rake task' do + let(:enable_registry) { true } + before :all do Rake.application.rake_require 'tasks/gitlab/task_helpers' Rake.application.rake_require 'tasks/gitlab/backup' @@ -15,13 +17,17 @@ describe 'gitlab:app namespace rake task' do FileUtils.mkdir_p('public/uploads') end + before do + stub_container_registry_config(enabled: enable_registry) + end + def run_rake_task(task_name) Rake::Task[task_name].reenable Rake.application.invoke_task task_name end def reenable_backup_sub_tasks - %w{db repo uploads builds artifacts lfs}.each do |subtask| + %w{db repo uploads builds artifacts lfs registry}.each do |subtask| Rake::Task["gitlab:backup:#{subtask}:create"].reenable end end @@ -65,6 +71,7 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke) expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke) expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke) + expect(Rake::Task['gitlab:backup:registry:restore']).to receive(:invoke) expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke) expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error end @@ -122,7 +129,7 @@ describe 'gitlab:app namespace rake task' do it 'should set correct permissions on the tar contents' do tar_contents, exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz} + %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz} ) expect(exit_status).to eq(0) expect(tar_contents).to match('db/') @@ -131,16 +138,29 @@ describe 'gitlab:app namespace rake task' do expect(tar_contents).to match('builds.tar.gz') expect(tar_contents).to match('artifacts.tar.gz') expect(tar_contents).to match('lfs.tar.gz') - expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz)\/$/) + expect(tar_contents).to match('registry.tar.gz') + expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/) end it 'should delete temp directories' do temp_dirs = Dir.glob( - File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs}') + File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs,registry}') ) expect(temp_dirs).to be_empty end + + context 'registry disabled' do + let(:enable_registry) { false } + + it 'should not create registry.tar.gz' do + tar_contents, exit_status = Gitlab::Popen.popen( + %W{tar -tvf #{@backup_tar}} + ) + expect(exit_status).to eq(0) + expect(tar_contents).not_to match('registry.tar.gz') + end + end end # backup_create task describe "Skipping items" do @@ -172,7 +192,7 @@ describe 'gitlab:app namespace rake task' do it "does not contain skipped item" do tar_contents, _exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz} + %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz} ) expect(tar_contents).to match('db/') @@ -180,6 +200,7 @@ describe 'gitlab:app namespace rake task' do expect(tar_contents).to match('builds.tar.gz') expect(tar_contents).to match('artifacts.tar.gz') expect(tar_contents).to match('lfs.tar.gz') + expect(tar_contents).to match('registry.tar.gz') expect(tar_contents).not_to match('repositories/') end @@ -195,6 +216,7 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke + expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke expect(Rake::Task['gitlab:shell:setup']).to receive :invoke expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error end diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb new file mode 100644 index 00000000000..36d03a224e4 --- /dev/null +++ b/spec/tasks/gitlab/db_rake_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' +require 'rake' + +describe 'gitlab:db namespace rake task' do + before :all do + Rake.application.rake_require 'active_record/railties/databases' + Rake.application.rake_require 'tasks/seed_fu' + Rake.application.rake_require 'tasks/gitlab/db' + + # empty task as env is already loaded + Rake::Task.define_task :environment + end + + before do + # Stub out db tasks + allow(Rake::Task['db:migrate']).to receive(:invoke).and_return(true) + allow(Rake::Task['db:schema:load']).to receive(:invoke).and_return(true) + allow(Rake::Task['db:seed_fu']).to receive(:invoke).and_return(true) + end + + describe 'configure' do + it 'should invoke db:migrate when schema has already been loaded' do + allow(ActiveRecord::Base.connection).to receive(:tables).and_return(['default']) + expect(Rake::Task['db:migrate']).to receive(:invoke) + expect(Rake::Task['db:schema:load']).not_to receive(:invoke) + expect(Rake::Task['db:seed_fu']).not_to receive(:invoke) + expect { run_rake_task('gitlab:db:configure') }.not_to raise_error + end + + it 'should invoke db:shema:load and db:seed_fu when schema is not loaded' do + allow(ActiveRecord::Base.connection).to receive(:tables).and_return([]) + expect(Rake::Task['db:schema:load']).to receive(:invoke) + expect(Rake::Task['db:seed_fu']).to receive(:invoke) + expect(Rake::Task['db:migrate']).not_to receive(:invoke) + expect { run_rake_task('gitlab:db:configure') }.not_to raise_error + end + + it 'should not invoke any other rake tasks during an error' do + allow(ActiveRecord::Base).to receive(:connection).and_raise(RuntimeError, 'error') + expect(Rake::Task['db:migrate']).not_to receive(:invoke) + expect(Rake::Task['db:schema:load']).not_to receive(:invoke) + expect(Rake::Task['db:seed_fu']).not_to receive(:invoke) + expect { run_rake_task('gitlab:db:configure') }.to raise_error(RuntimeError, 'error') + # unstub connection so that the database cleaner still works + allow(ActiveRecord::Base).to receive(:connection).and_call_original + end + + it 'should not invoke seed after a failed schema_load' do + allow(ActiveRecord::Base.connection).to receive(:tables).and_return([]) + allow(Rake::Task['db:schema:load']).to receive(:invoke).and_raise(RuntimeError, 'error') + expect(Rake::Task['db:schema:load']).to receive(:invoke) + expect(Rake::Task['db:seed_fu']).not_to receive(:invoke) + expect(Rake::Task['db:migrate']).not_to receive(:invoke) + expect { run_rake_task('gitlab:db:configure') }.to raise_error(RuntimeError, 'error') + end + end + + def run_rake_task(task_name) + Rake::Task[task_name].reenable + Rake.application.invoke_task task_name + end +end diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb index 58f45ff8610..69b2b9b6d5b 100644 --- a/spec/teaspoon_env.rb +++ b/spec/teaspoon_env.rb @@ -41,11 +41,11 @@ Teaspoon.configure do |config| suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee}" # Load additional JS files, but requiring them in your spec helper is the preferred way to do this. - #suite.javascripts = [] + # suite.javascripts = [] # You can include your own stylesheets if you want to change how Teaspoon looks. # Note: Spec related CSS can and should be loaded using fixtures. - #suite.stylesheets = ["teaspoon"] + # suite.stylesheets = ["teaspoon"] # This suites spec helper, which can require additional support files. This file is loaded before any of your test # files are loaded. @@ -62,19 +62,19 @@ Teaspoon.configure do |config| # Hooks allow you to use `Teaspoon.hook("fixtures")` before, after, or during your spec run. This will make a # synchronous Ajax request to the server that will call all of the blocks you've defined for that hook name. - #suite.hook :fixtures, &proc{} + # suite.hook :fixtures, &proc{} # Determine whether specs loaded into the test harness should be embedded as individual script tags or concatenated - # into a single file. Similar to Rails' asset `debug: true` and `config.assets.debug = true` options. By default, + # into a single file. Similar to Rails' asset `debug: true` and `config.assets.debug = true` options. By default, # Teaspoon expands all assets to provide more valuable stack traces that reference individual source files. - #suite.expand_assets = true + # suite.expand_assets = true end # Example suite. Since we're just filtering to files already within the root test/javascripts, these files will also # be run in the default suite -- but can be focused into a more specific suite. - #config.suite :targeted do |suite| + # config.suite :targeted do |suite| # suite.matcher = "spec/javascripts/targeted/*_spec.{js,js.coffee,coffee}" - #end + # end # CONSOLE RUNNER SPECIFIC # @@ -94,45 +94,45 @@ Teaspoon.configure do |config| # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit - #config.driver = :phantomjs + # config.driver = :phantomjs # Specify additional options for the driver. # # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit - #config.driver_options = nil + # config.driver_options = nil # Specify the timeout for the driver. Specs are expected to complete within this time frame or the run will be # considered a failure. This is to avoid issues that can arise where tests stall. - #config.driver_timeout = 180 + # config.driver_timeout = 180 # Specify a server to use with Rack (e.g. thin, mongrel). If nil is provided Rack::Server is used. - #config.server = nil + # config.server = nil # Specify a port to run on a specific port, otherwise Teaspoon will use a random available port. - #config.server_port = nil + # config.server_port = nil # Timeout for starting the server in seconds. If your server is slow to start you may have to bump this, or you may # want to lower this if you know it shouldn't take long to start. - #config.server_timeout = 20 + # config.server_timeout = 20 # Force Teaspoon to fail immediately after a failing suite. Can be useful to make Teaspoon fail early if you have # several suites, but in environments like CI this may not be desirable. - #config.fail_fast = true + # config.fail_fast = true # Specify the formatters to use when outputting the results. # Note: Output files can be specified by using `"junit>/path/to/output.xml"`. # # Available: :dot, :clean, :documentation, :json, :junit, :pride, :rspec_html, :snowday, :swayze_or_oprah, :tap, :tap_y, :teamcity - #config.formatters = [:dot] + # config.formatters = [:dot] # Specify if you want color output from the formatters. - #config.color = true + # config.color = true # Teaspoon pipes all console[log/debug/error] to $stdout. This is useful to catch places where you've forgotten to # remove them, but in verbose applications this may not be desirable. - #config.suppress_log = false + # config.suppress_log = false # COVERAGE REPORTS / THRESHOLD ASSERTIONS # @@ -149,7 +149,7 @@ Teaspoon.configure do |config| # Specify that you always want a coverage configuration to be used. Otherwise, specify that you want coverage # on the CLI. # Set this to "true" or the name of your coverage config. - #config.use_coverage = nil + # config.use_coverage = nil # You can have multiple coverage configs by passing a name to config.coverage. # e.g. config.coverage :ci do |coverage| @@ -158,21 +158,21 @@ Teaspoon.configure do |config| # Which coverage reports Istanbul should generate. Correlates directly to what Istanbul supports. # # Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity - #coverage.reports = ["text-summary", "html"] + # coverage.reports = ["text-summary", "html"] # The path that the coverage should be written to - when there's an artifact to write to disk. # Note: Relative to `config.root`. - #coverage.output_path = "coverage" + # coverage.output_path = "coverage" # Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The # default excludes assets from vendor, gems and support libraries. - #coverage.ignore = [%r{/lib/ruby/gems/}, %r{/vendor/assets/}, %r{/support/}, %r{/(.+)_helper.}] + # coverage.ignore = [%r{/lib/ruby/gems/}, %r{/vendor/assets/}, %r{/support/}, %r{/(.+)_helper.}] # Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any # aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil. - #coverage.statements = nil - #coverage.functions = nil - #coverage.branches = nil - #coverage.lines = nil + # coverage.statements = nil + # coverage.functions = nil + # coverage.branches = nil + # coverage.lines = nil end end diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb index 3600c771075..439da765c2c 100644 --- a/spec/workers/emails_on_push_worker_spec.rb +++ b/spec/workers/emails_on_push_worker_spec.rb @@ -6,29 +6,66 @@ describe EmailsOnPushWorker do let(:project) { create(:project) } let(:user) { create(:user) } let(:data) { Gitlab::PushDataBuilder.build_sample(project, user) } + let(:recipients) { user.email } + let(:perform) { subject.perform(project.id, recipients, data.stringify_keys) } subject { EmailsOnPushWorker.new } - before do - allow(Project).to receive(:find).and_return(project) - end - describe "#perform" do - it "sends mail" do - subject.perform(project.id, user.email, data.stringify_keys) + context "when there are no errors in sending" do + let(:email) { ActionMailer::Base.deliveries.last } + + before { perform } - email = ActionMailer::Base.deliveries.last - expect(email.subject).to include('Change some files') - expect(email.to).to eq([user.email]) + it "sends a mail with the correct subject" do + expect(email.subject).to include('Change some files') + end + + it "sends the mail to the correct recipient" do + expect(email.to).to eq([user.email]) + end end - it "gracefully handles an input SMTP error" do - ActionMailer::Base.deliveries.clear - allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError) + context "when there is an SMTP error" do + before do + ActionMailer::Base.deliveries.clear + allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError) + perform + end + + it "gracefully handles an input SMTP error" do + expect(ActionMailer::Base.deliveries.count).to eq(0) + end + end + + context "when there are multiple recipients" do + let(:recipients) do + 1.upto(5).map { |i| user.email.sub('@', "+#{i}@") }.join("\n") + end + + before do + # This is a hack because we modify the mail object before sending, for efficency, + # but the TestMailer adapter just appends the objects to an array. To clone a mail + # object, create a new one! + # https://github.com/mikel/mail/issues/314#issuecomment-12750108 + allow_any_instance_of(Mail::TestMailer).to receive(:deliver!).and_wrap_original do |original, mail| + original.call(Mail.new(mail.encoded)) + end + + ActionMailer::Base.deliveries.clear + end - subject.perform(project.id, user.email, data.stringify_keys) + it "sends the mail to each of the recipients" do + perform + expect(ActionMailer::Base.deliveries.count).to eq(5) + expect(ActionMailer::Base.deliveries.map(&:to).flatten).to contain_exactly(*recipients.split) + end - expect(ActionMailer::Base.deliveries.count).to eq(0) + it "only generates the mail once" do + expect(Notify).to receive(:repository_push_email).once.and_call_original + expect(Premailer::Rails::CustomizedPremailer).to receive(:new).once.and_call_original + perform + end end end end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 94ff3457902..b8e73682c91 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -48,6 +48,22 @@ describe PostReceive do PostReceive.new.perform(pwd(project), key_id, base64_changes) end end + + context "gitlab-ci.yml" do + subject { PostReceive.new.perform(pwd(project), key_id, base64_changes) } + + context "creates a Ci::Pipeline for every change" do + before { stub_ci_pipeline_to_return_yaml_file } + + it { expect{ subject }.to change{ Ci::Pipeline.count }.by(2) } + end + + context "does not create a Ci::Pipeline" do + before { stub_ci_pipeline_yaml_file(nil) } + + it { expect{ subject }.not_to change{ Ci::Pipeline.count } } + end + end end context "webhook" do diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb index 6739063543b..f1b1574abf4 100644 --- a/spec/workers/repository_import_worker_spec.rb +++ b/spec/workers/repository_import_worker_spec.rb @@ -6,14 +6,28 @@ describe RepositoryImportWorker do subject { described_class.new } describe '#perform' do - it 'imports a project' do - expect_any_instance_of(Projects::ImportService).to receive(:execute). - and_return({ status: :ok }) + context 'when the import was successful' do + it 'imports a project' do + expect_any_instance_of(Projects::ImportService).to receive(:execute). + and_return({ status: :ok }) - expect_any_instance_of(Repository).to receive(:expire_emptiness_caches) - expect_any_instance_of(Project).to receive(:import_finish) + expect_any_instance_of(Repository).to receive(:expire_emptiness_caches) + expect_any_instance_of(Project).to receive(:import_finish) - subject.perform(project.id) + subject.perform(project.id) + end + end + + context 'when the import has failed' do + it 'hide the credentials that were used in the import URL' do + error = %Q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found } + expect_any_instance_of(Projects::ImportService).to receive(:execute). + and_return({ status: :error, message: error }) + + subject.perform(project.id) + + expect(project.reload.import_error).to include("https://*****:*****@test.com/root/repoC.git/") + end end end end diff --git a/vendor/assets/javascripts/jquery.scrollTo.js b/vendor/assets/javascripts/jquery.scrollTo.js new file mode 100755 index 00000000000..7ba17766b70 --- /dev/null +++ b/vendor/assets/javascripts/jquery.scrollTo.js @@ -0,0 +1,210 @@ +/*! + * jQuery.scrollTo + * Copyright (c) 2007-2015 Ariel Flesler - aflesler<a>gmail<d>com | http://flesler.blogspot.com + * Licensed under MIT + * http://flesler.blogspot.com/2007/10/jqueryscrollto.html + * @projectDescription Lightweight, cross-browser and highly customizable animated scrolling with jQuery + * @author Ariel Flesler + * @version 2.1.2 + */ +;(function(factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // AMD + define(['jquery'], factory); + } else if (typeof module !== 'undefined' && module.exports) { + // CommonJS + module.exports = factory(require('jquery')); + } else { + // Global + factory(jQuery); + } +})(function($) { + 'use strict'; + + var $scrollTo = $.scrollTo = function(target, duration, settings) { + return $(window).scrollTo(target, duration, settings); + }; + + $scrollTo.defaults = { + axis:'xy', + duration: 0, + limit:true + }; + + function isWin(elem) { + return !elem.nodeName || + $.inArray(elem.nodeName.toLowerCase(), ['iframe','#document','html','body']) !== -1; + } + + $.fn.scrollTo = function(target, duration, settings) { + if (typeof duration === 'object') { + settings = duration; + duration = 0; + } + if (typeof settings === 'function') { + settings = { onAfter:settings }; + } + if (target === 'max') { + target = 9e9; + } + + settings = $.extend({}, $scrollTo.defaults, settings); + // Speed is still recognized for backwards compatibility + duration = duration || settings.duration; + // Make sure the settings are given right + var queue = settings.queue && settings.axis.length > 1; + if (queue) { + // Let's keep the overall duration + duration /= 2; + } + settings.offset = both(settings.offset); + settings.over = both(settings.over); + + return this.each(function() { + // Null target yields nothing, just like jQuery does + if (target === null) return; + + var win = isWin(this), + elem = win ? this.contentWindow || window : this, + $elem = $(elem), + targ = target, + attr = {}, + toff; + + switch (typeof targ) { + // A number will pass the regex + case 'number': + case 'string': + if (/^([+-]=?)?\d+(\.\d+)?(px|%)?$/.test(targ)) { + targ = both(targ); + // We are done + break; + } + // Relative/Absolute selector + targ = win ? $(targ) : $(targ, elem); + /* falls through */ + case 'object': + if (targ.length === 0) return; + // DOMElement / jQuery + if (targ.is || targ.style) { + // Get the real position of the target + toff = (targ = $(targ)).offset(); + } + } + + var offset = $.isFunction(settings.offset) && settings.offset(elem, targ) || settings.offset; + + $.each(settings.axis.split(''), function(i, axis) { + var Pos = axis === 'x' ? 'Left' : 'Top', + pos = Pos.toLowerCase(), + key = 'scroll' + Pos, + prev = $elem[key](), + max = $scrollTo.max(elem, axis); + + if (toff) {// jQuery / DOMElement + attr[key] = toff[pos] + (win ? 0 : prev - $elem.offset()[pos]); + + // If it's a dom element, reduce the margin + if (settings.margin) { + attr[key] -= parseInt(targ.css('margin'+Pos), 10) || 0; + attr[key] -= parseInt(targ.css('border'+Pos+'Width'), 10) || 0; + } + + attr[key] += offset[pos] || 0; + + if (settings.over[pos]) { + // Scroll to a fraction of its width/height + attr[key] += targ[axis === 'x'?'width':'height']() * settings.over[pos]; + } + } else { + var val = targ[pos]; + // Handle percentage values + attr[key] = val.slice && val.slice(-1) === '%' ? + parseFloat(val) / 100 * max + : val; + } + + // Number or 'number' + if (settings.limit && /^\d+$/.test(attr[key])) { + // Check the limits + attr[key] = attr[key] <= 0 ? 0 : Math.min(attr[key], max); + } + + // Don't waste time animating, if there's no need. + if (!i && settings.axis.length > 1) { + if (prev === attr[key]) { + // No animation needed + attr = {}; + } else if (queue) { + // Intermediate animation + animate(settings.onAfterFirst); + // Don't animate this axis again in the next iteration. + attr = {}; + } + } + }); + + animate(settings.onAfter); + + function animate(callback) { + var opts = $.extend({}, settings, { + // The queue setting conflicts with animate() + // Force it to always be true + queue: true, + duration: duration, + complete: callback && function() { + callback.call(elem, targ, settings); + } + }); + $elem.animate(attr, opts); + } + }); + }; + + // Max scrolling position, works on quirks mode + // It only fails (not too badly) on IE, quirks mode. + $scrollTo.max = function(elem, axis) { + var Dim = axis === 'x' ? 'Width' : 'Height', + scroll = 'scroll'+Dim; + + if (!isWin(elem)) + return elem[scroll] - $(elem)[Dim.toLowerCase()](); + + var size = 'client' + Dim, + doc = elem.ownerDocument || elem.document, + html = doc.documentElement, + body = doc.body; + + return Math.max(html[scroll], body[scroll]) - Math.min(html[size], body[size]); + }; + + function both(val) { + return $.isFunction(val) || $.isPlainObject(val) ? val : { top:val, left:val }; + } + + // Add special hooks so that window scroll properties can be animated + $.Tween.propHooks.scrollLeft = + $.Tween.propHooks.scrollTop = { + get: function(t) { + return $(t.elem)[t.prop](); + }, + set: function(t) { + var curr = this.get(t); + // If interrupt is true and user scrolled, stop animating + if (t.options.interrupt && t._last && t._last !== curr) { + return $(t.elem).stop(); + } + var next = Math.round(t.now); + // Don't waste CPU + // Browsers don't render floating point scroll + if (curr !== next) { + $(t.elem)[t.prop](next); + t._last = this.get(t); + } + } + }; + + // AMD requirement + return $scrollTo; +}); diff --git a/vendor/assets/javascripts/task_list.js.coffee b/vendor/assets/javascripts/task_list.js.coffee new file mode 100644 index 00000000000..584751af8ea --- /dev/null +++ b/vendor/assets/javascripts/task_list.js.coffee @@ -0,0 +1,258 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 GitHub, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# TaskList Behavior +# +#= provides tasklist:enabled +#= provides tasklist:disabled +#= provides tasklist:change +#= provides tasklist:changed +# +# +# Enables Task List update behavior. +# +# ### Example Markup +# +# <div class="js-task-list-container"> +# <ul class="task-list"> +# <li class="task-list-item"> +# <input type="checkbox" class="js-task-list-item-checkbox" disabled /> +# text +# </li> +# </ul> +# <form> +# <textarea class="js-task-list-field">- [ ] text</textarea> +# </form> +# </div> +# +# ### Specification +# +# TaskLists MUST be contained in a `(div).js-task-list-container`. +# +# TaskList Items SHOULD be an a list (`UL`/`OL`) element. +# +# Task list items MUST match `(input).task-list-item-checkbox` and MUST be +# `disabled` by default. +# +# TaskLists MUST have a `(textarea).js-task-list-field` form element whose +# `value` attribute is the source (Markdown) to be udpated. The source MUST +# follow the syntax guidelines. +# +# TaskList updates trigger `tasklist:change` events. If the change is +# successful, `tasklist:changed` is fired. The change can be canceled. +# +# jQuery is required. +# +# ### Methods +# +# `.taskList('enable')` or `.taskList()` +# +# Enables TaskList updates for the container. +# +# `.taskList('disable')` +# +# Disables TaskList updates for the container. +# +## ### Events +# +# `tasklist:enabled` +# +# Fired when the TaskList is enabled. +# +# * **Synchronicity** Sync +# * **Bubbles** Yes +# * **Cancelable** No +# * **Target** `.js-task-list-container` +# +# `tasklist:disabled` +# +# Fired when the TaskList is disabled. +# +# * **Synchronicity** Sync +# * **Bubbles** Yes +# * **Cancelable** No +# * **Target** `.js-task-list-container` +# +# `tasklist:change` +# +# Fired before the TaskList item change takes affect. +# +# * **Synchronicity** Sync +# * **Bubbles** Yes +# * **Cancelable** Yes +# * **Target** `.js-task-list-field` +# +# `tasklist:changed` +# +# Fired once the TaskList item change has taken affect. +# +# * **Synchronicity** Sync +# * **Bubbles** Yes +# * **Cancelable** No +# * **Target** `.js-task-list-field` +# +# ### NOTE +# +# Task list checkboxes are rendered as disabled by default because rendered +# user content is cached without regard for the viewer. + +incomplete = "[ ]" +complete = "[x]" + +# Escapes the String for regular expression matching. +escapePattern = (str) -> + str. + replace(/([\[\]])/g, "\\$1"). # escape square brackets + replace(/\s/, "\\s"). # match all white space + replace("x", "[xX]") # match all cases + +incompletePattern = /// + #{escapePattern(incomplete)} +/// +completePattern = /// + #{escapePattern(complete)} +/// + +# Pattern used to identify all task list items. +# Useful when you need iterate over all items. +itemPattern = /// + ^ + (?: # prefix, consisting of + \s* # optional leading whitespace + (?:>\s*)* # zero or more blockquotes + (?:[-+*]|(?:\d+\.)) # list item indicator + ) + \s* # optional whitespace prefix + ( # checkbox + #{escapePattern(complete)}| + #{escapePattern(incomplete)} + ) + \s+ # is followed by whitespace + (?! + \(.*?\) # is not part of a [foo](url) link + ) + (?= # and is followed by zero or more links + (?:\[.*?\]\s*(?:\[.*?\]|\(.*?\))\s*)* + (?:[^\[]|$) # and either a non-link or the end of the string + ) +/// + +# Used to filter out code fences from the source for comparison only. +# http://rubular.com/r/x5EwZVrloI +# Modified slightly due to issues with JS +codeFencesPattern = /// + ^`{3} # ``` + (?:\s*\w+)? # followed by optional language + [\S\s] # whitespace + .* # code + [\S\s] # whitespace + ^`{3}$ # ``` +///mg + +# Used to filter out potential mismatches (items not in lists). +# http://rubular.com/r/OInl6CiePy +itemsInParasPattern = /// + ^ + ( + #{escapePattern(complete)}| + #{escapePattern(incomplete)} + ) + .+ + $ +///g + +# Given the source text, updates the appropriate task list item to match the +# given checked value. +# +# Returns the updated String text. +updateTaskListItem = (source, itemIndex, checked) -> + clean = source.replace(/\r/g, '').replace(codeFencesPattern, ''). + replace(itemsInParasPattern, '').split("\n") + index = 0 + result = for line in source.split("\n") + if line in clean && line.match(itemPattern) + index += 1 + if index == itemIndex + line = + if checked + line.replace(incompletePattern, complete) + else + line.replace(completePattern, incomplete) + line + result.join("\n") + +# Updates the $field value to reflect the state of $item. +# Triggers the `tasklist:change` event before the value has changed, and fires +# a `tasklist:changed` event once the value has changed. +updateTaskList = ($item) -> + $container = $item.closest '.js-task-list-container' + $field = $container.find '.js-task-list-field' + index = 1 + $container.find('.task-list-item-checkbox').index($item) + checked = $item.prop 'checked' + + event = $.Event 'tasklist:change' + $field.trigger event, [index, checked] + + unless event.isDefaultPrevented() + $field.val updateTaskListItem($field.val(), index, checked) + $field.trigger 'change' + $field.trigger 'tasklist:changed', [index, checked] + +# When the task list item checkbox is updated, submit the change +$(document).on 'change', '.task-list-item-checkbox', -> + updateTaskList $(this) + +# Enables TaskList item changes. +enableTaskList = ($container) -> + if $container.find('.js-task-list-field').length > 0 + $container. + find('.task-list-item').addClass('enabled'). + find('.task-list-item-checkbox').attr('disabled', null) + $container.addClass('is-task-list-enabled'). + trigger 'tasklist:enabled' + +# Enables a collection of TaskList containers. +enableTaskLists = ($containers) -> + for container in $containers + enableTaskList $(container) + +# Disable TaskList item changes. +disableTaskList = ($container) -> + $container. + find('.task-list-item').removeClass('enabled'). + find('.task-list-item-checkbox').attr('disabled', 'disabled') + $container.removeClass('is-task-list-enabled'). + trigger 'tasklist:disabled' + +# Disables a collection of TaskList containers. +disableTaskLists = ($containers) -> + for container in $containers + disableTaskList $(container) + +$.fn.taskList = (method) -> + $container = $(this).closest('.js-task-list-container') + + methods = + enable: enableTaskLists + disable: disableTaskLists + + methods[method || 'enable']($container) diff --git a/vendor/assets/javascripts/u2f.js b/vendor/assets/javascripts/u2f.js new file mode 100644 index 00000000000..e666b136051 --- /dev/null +++ b/vendor/assets/javascripts/u2f.js @@ -0,0 +1,748 @@ +//Copyright 2014-2015 Google Inc. All rights reserved. + +//Use of this source code is governed by a BSD-style +//license that can be found in the LICENSE file or at +//https://developers.google.com/open-source/licenses/bsd + +/** + * @fileoverview The U2F api. + */ +'use strict'; + + +/** + * Namespace for the U2F api. + * @type {Object} + */ +var u2f = u2f || {}; + +/** + * FIDO U2F Javascript API Version + * @number + */ +var js_api_version; + +/** + * The U2F extension id + * @const {string} + */ +// The Chrome packaged app extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the package Chrome app and does not require installing the U2F Chrome extension. +u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; +// The U2F Chrome extension ID. +// Uncomment this if you want to deploy a server instance that uses +// the U2F Chrome extension to authenticate. +// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne'; + + +/** + * Message types for messsages to/from the extension + * @const + * @enum {string} + */ +u2f.MessageTypes = { + 'U2F_REGISTER_REQUEST': 'u2f_register_request', + 'U2F_REGISTER_RESPONSE': 'u2f_register_response', + 'U2F_SIGN_REQUEST': 'u2f_sign_request', + 'U2F_SIGN_RESPONSE': 'u2f_sign_response', + 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request', + 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response' +}; + + +/** + * Response status codes + * @const + * @enum {number} + */ +u2f.ErrorCodes = { + 'OK': 0, + 'OTHER_ERROR': 1, + 'BAD_REQUEST': 2, + 'CONFIGURATION_UNSUPPORTED': 3, + 'DEVICE_INELIGIBLE': 4, + 'TIMEOUT': 5 +}; + + +/** + * A message for registration requests + * @typedef {{ + * type: u2f.MessageTypes, + * appId: ?string, + * timeoutSeconds: ?number, + * requestId: ?number + * }} + */ +u2f.U2fRequest; + + +/** + * A message for registration responses + * @typedef {{ + * type: u2f.MessageTypes, + * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), + * requestId: ?number + * }} + */ +u2f.U2fResponse; + + +/** + * An error object for responses + * @typedef {{ + * errorCode: u2f.ErrorCodes, + * errorMessage: ?string + * }} + */ +u2f.Error; + +/** + * Data object for a single sign request. + * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}} + */ +u2f.Transport; + + +/** + * Data object for a single sign request. + * @typedef {Array<u2f.Transport>} + */ +u2f.Transports; + +/** + * Data object for a single sign request. + * @typedef {{ + * version: string, + * challenge: string, + * keyHandle: string, + * appId: string + * }} + */ +u2f.SignRequest; + + +/** + * Data object for a sign response. + * @typedef {{ + * keyHandle: string, + * signatureData: string, + * clientData: string + * }} + */ +u2f.SignResponse; + + +/** + * Data object for a registration request. + * @typedef {{ + * version: string, + * challenge: string + * }} + */ +u2f.RegisterRequest; + + +/** + * Data object for a registration response. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: Transports, + * appId: string + * }} + */ +u2f.RegisterResponse; + + +/** + * Data object for a registered key. + * @typedef {{ + * version: string, + * keyHandle: string, + * transports: ?Transports, + * appId: ?string + * }} + */ +u2f.RegisteredKey; + + +/** + * Data object for a get API register response. + * @typedef {{ + * js_api_version: number + * }} + */ +u2f.GetJsApiVersionResponse; + + +//Low level MessagePort API support + +/** + * Sets up a MessagePort to the U2F extension using the + * available mechanisms. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + */ +u2f.getMessagePort = function(callback) { + if (typeof chrome != 'undefined' && chrome.runtime) { + // The actual message here does not matter, but we need to get a reply + // for the callback to run. Thus, send an empty signature request + // in order to get a failure response. + var msg = { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: [] + }; + chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { + if (!chrome.runtime.lastError) { + // We are on a whitelisted origin and can talk directly + // with the extension. + u2f.getChromeRuntimePort_(callback); + } else { + // chrome.runtime was available, but we couldn't message + // the extension directly, use iframe + u2f.getIframePort_(callback); + } + }); + } else if (u2f.isAndroidChrome_()) { + u2f.getAuthenticatorPort_(callback); + } else if (u2f.isIosChrome_()) { + u2f.getIosPort_(callback); + } else { + // chrome.runtime was not available at all, which is normal + // when this origin doesn't have access to any extensions. + u2f.getIframePort_(callback); + } +}; + +/** + * Detect chrome running on android based on the browser's useragent. + * @private + */ +u2f.isAndroidChrome_ = function() { + var userAgent = navigator.userAgent; + return userAgent.indexOf('Chrome') != -1 && + userAgent.indexOf('Android') != -1; +}; + +/** + * Detect chrome running on iOS based on the browser's platform. + * @private + */ +u2f.isIosChrome_ = function() { + return $.inArray(navigator.platform, ["iPhone", "iPad", "iPod"]) > -1; +}; + +/** + * Connects directly to the extension via chrome.runtime.connect. + * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * @private + */ +u2f.getChromeRuntimePort_ = function(callback) { + var port = chrome.runtime.connect(u2f.EXTENSION_ID, + {'includeTlsChannelId': true}); + setTimeout(function() { + callback(new u2f.WrappedChromeRuntimePort_(port)); + }, 0); +}; + +/** + * Return a 'port' abstraction to the Authenticator app. + * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * @private + */ +u2f.getAuthenticatorPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedAuthenticatorPort_()); + }, 0); +}; + +/** + * Return a 'port' abstraction to the iOS client app. + * @param {function(u2f.WrappedIosPort_)} callback + * @private + */ +u2f.getIosPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedIosPort_()); + }, 0); +}; + +/** + * A wrapper for chrome.runtime.Port that is compatible with MessagePort. + * @param {Port} port + * @constructor + * @private + */ +u2f.WrappedChromeRuntimePort_ = function(port) { + this.port_ = port; +}; + +/** + * Format and return a sign request compliant with the JS API version supported by the extension. + * @param {Array<u2f.SignRequest>} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatSignRequest_ = + function(appId, challenge, registeredKeys, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: challenge, + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: signRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + appId: appId, + challenge: challenge, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + }; + +/** + * Format and return a register request compliant with the JS API version supported by the extension.. + * @param {Array<u2f.SignRequest>} signRequests + * @param {Array<u2f.RegisterRequest>} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.formatRegisterRequest_ = + function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) { + if (js_api_version === undefined || js_api_version < 1.1) { + // Adapt request to the 1.0 JS API + for (var i = 0; i < registerRequests.length; i++) { + registerRequests[i].appId = appId; + } + var signRequests = []; + for (var i = 0; i < registeredKeys.length; i++) { + signRequests[i] = { + version: registeredKeys[i].version, + challenge: registerRequests[0], + keyHandle: registeredKeys[i].keyHandle, + appId: appId + }; + } + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + signRequests: signRequests, + registerRequests: registerRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + } + // JS 1.1 API + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + appId: appId, + registerRequests: registerRequests, + registeredKeys: registeredKeys, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + }; + + +/** + * Posts a message on the underlying channel. + * @param {Object} message + */ +u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { + this.port_.postMessage(message); +}; + + +/** + * Emulates the HTML 5 addEventListener interface. Works only for the + * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedChromeRuntimePort_.prototype.addEventListener = + function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message' || name == 'onmessage') { + this.port_.onMessage.addListener(function(message) { + // Emulate a minimal MessageEvent object + handler({'data': message}); + }); + } else { + console.error('WrappedChromeRuntimePort only supports onMessage'); + } + }; + +/** + * Wrap the Authenticator app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedAuthenticatorPort_ = function() { + this.requestId_ = -1; + this.requestObject_ = null; +} + +/** + * Launch the Authenticator intent. + * @param {Object} message + */ +u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.request=' + encodeURIComponent(JSON.stringify(message)) + + ';end'; + document.location = intentUrl; +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() { + return "WrappedAuthenticatorPort_"; +}; + + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message') { + var self = this; + /* Register a callback to that executes when + * chrome injects the response. */ + window.addEventListener( + 'message', self.onRequestUpdate_.bind(self, handler), false); + } else { + console.error('WrappedAuthenticatorPort only supports message'); + } +}; + +/** + * Callback invoked when a response is received from the Authenticator. + * @param function({data: Object}) callback + * @param {Object} message message Object + */ +u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = + function(callback, message) { + var messageObject = JSON.parse(message.data); + var intentUrl = messageObject['intentURL']; + + var errorCode = messageObject['errorCode']; + var responseObject = null; + if (messageObject.hasOwnProperty('data')) { + responseObject = /** @type {Object} */ ( + JSON.parse(messageObject['data'])); + } + + callback({'data': responseObject}); + }; + +/** + * Base URL for intents to Authenticator. + * @const + * @private + */ +u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = + 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; + +/** + * Wrap the iOS client app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedIosPort_ = function() {}; + +/** + * Launch the iOS client app request + * @param {Object} message + */ +u2f.WrappedIosPort_.prototype.postMessage = function(message) { + var str = JSON.stringify(message); + var url = "u2f://auth?" + encodeURI(str); + location.replace(url); +}; + +/** + * Tells what type of port this is. + * @return {String} port type + */ +u2f.WrappedIosPort_.prototype.getPortType = function() { + return "WrappedIosPort_"; +}; + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name !== 'message') { + console.error('WrappedIosPort only supports message'); + } +}; + +/** + * Sets up an embedded trampoline iframe, sourced from the extension. + * @param {function(MessagePort)} callback + * @private + */ +u2f.getIframePort_ = function(callback) { + // Create the iframe + var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; + var iframe = document.createElement('iframe'); + iframe.src = iframeOrigin + '/u2f-comms.html'; + iframe.setAttribute('style', 'display:none'); + document.body.appendChild(iframe); + + var channel = new MessageChannel(); + var ready = function(message) { + if (message.data == 'ready') { + channel.port1.removeEventListener('message', ready); + callback(channel.port1); + } else { + console.error('First event on iframe port was not "ready"'); + } + }; + channel.port1.addEventListener('message', ready); + channel.port1.start(); + + iframe.addEventListener('load', function() { + // Deliver the port to the iframe and initialize + iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); + }); +}; + + +//High-level JS API + +/** + * Default extension response timeout in seconds. + * @const + */ +u2f.EXTENSION_TIMEOUT_SEC = 30; + +/** + * A singleton instance for a MessagePort to the extension. + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} + * @private + */ +u2f.port_ = null; + +/** + * Callbacks waiting for a port + * @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>} + * @private + */ +u2f.waitingForPort_ = []; + +/** + * A counter for requestIds. + * @type {number} + * @private + */ +u2f.reqCounter_ = 0; + +/** + * A map from requestIds to client callbacks + * @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse)) + * |function((u2f.Error|u2f.SignResponse)))>} + * @private + */ +u2f.callbackMap_ = {}; + +/** + * Creates or retrieves the MessagePort singleton to use. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * @private + */ +u2f.getPortSingleton_ = function(callback) { + if (u2f.port_) { + callback(u2f.port_); + } else { + if (u2f.waitingForPort_.length == 0) { + u2f.getMessagePort(function(port) { + u2f.port_ = port; + u2f.port_.addEventListener('message', + /** @type {function(Event)} */ (u2f.responseHandler_)); + + // Careful, here be async callbacks. Maybe. + while (u2f.waitingForPort_.length) + u2f.waitingForPort_.shift()(u2f.port_); + }); + } + u2f.waitingForPort_.push(callback); + } +}; + +/** + * Handles response messages from the extension. + * @param {MessageEvent.<u2f.Response>} message + * @private + */ +u2f.responseHandler_ = function(message) { + var response = message.data; + var reqId = response['requestId']; + if (!reqId || !u2f.callbackMap_[reqId]) { + console.error('Unknown or missing requestId in response.'); + return; + } + var cb = u2f.callbackMap_[reqId]; + delete u2f.callbackMap_[reqId]; + cb(response['responseData']); +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the sign request. + * @param {string=} appId + * @param {string=} challenge + * @param {Array<u2f.RegisteredKey>} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual sign request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual sign request in the supported API version. + u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * @param {string=} appId + * @param {string=} challenge + * @param {Array<u2f.RegisteredKey>} registeredKeys + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * If the JS API version supported by the extension is unknown, it first sends a + * message to the extension to find out the supported API version and then it sends + * the register request. + * @param {string=} appId + * @param {Array<u2f.RegisterRequest>} registerRequests + * @param {Array<u2f.RegisteredKey>} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + if (js_api_version === undefined) { + // Send a message to get the extension to JS API version, then send the actual register request. + u2f.getApiVersion( + function (response) { + js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version']; + console.log("Extension JS API Version: ", js_api_version); + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + }); + } else { + // We know the JS API version. Send the actual register request in the supported API version. + u2f.sendRegisterRequest(appId, registerRequests, registeredKeys, + callback, opt_timeoutSeconds); + } +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {string=} appId + * @param {Array<u2f.RegisterRequest>} registerRequests + * @param {Array<u2f.RegisteredKey>} registeredKeys + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = u2f.formatRegisterRequest_( + appId, registeredKeys, registerRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + + +/** + * Dispatches a message to the extension to find out the supported + * JS API version. + * If the user is on a mobile phone and is thus using Google Authenticator instead + * of the Chrome extension, don't send the request and simply return 0. + * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.getApiVersion = function(callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + // If we are using Android Google Authenticator or iOS client app, + // do not fire an intent to ask which JS API version to use. + if (port.getPortType) { + var apiVersion; + switch (port.getPortType()) { + case 'WrappedIosPort_': + case 'WrappedAuthenticatorPort_': + apiVersion = 1.1; + break; + + default: + apiVersion = 0; + break; + } + callback({ 'js_api_version': apiVersion }); + return; + } + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var req = { + type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST, + timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC), + requestId: reqId + }; + port.postMessage(req); + }); +};
\ No newline at end of file diff --git a/vendor/assets/stylesheets/animate.css b/vendor/assets/stylesheets/animate.css deleted file mode 100644 index b6f61295392..00000000000 --- a/vendor/assets/stylesheets/animate.css +++ /dev/null @@ -1,11 +0,0 @@ -@charset "UTF-8"; - -/*! - * animate.css -http://daneden.me/animate - * Version - 3.5.1 - * Licensed under the MIT license - http://opensource.org/licenses/MIT - * - * Copyright (c) 2016 Daniel Eden - */ - -.animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animated.hinge{-webkit-animation-duration:2s;animation-duration:2s}.animated.bounceIn,.animated.bounceOut,.animated.flipOutX,.animated.flipOutY{-webkit-animation-duration:.75s;animation-duration:.75s}@-webkit-keyframes bounce{0%,20%,53%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}40%,43%,70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}70%{-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}@keyframes bounce{0%,20%,53%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}40%,43%,70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}70%{-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}.bounce{-webkit-animation-name:bounce;animation-name:bounce;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.pulse{-webkit-animation-name:pulse;animation-name:pulse}@-webkit-keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.rubberBand{-webkit-animation-name:rubberBand;animation-name:rubberBand}@-webkit-keyframes shake{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes shake{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.shake{-webkit-animation-name:shake;animation-name:shake}@-webkit-keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}.headShake{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-name:headShake;animation-name:headShake}@-webkit-keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}.swing{-webkit-transform-origin:top center;transform-origin:top center;-webkit-animation-name:swing;animation-name:swing}@-webkit-keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.tada{-webkit-animation-name:tada;animation-name:tada}@-webkit-keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:none;transform:none}}@keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:none;transform:none}}.wobble{-webkit-animation-name:wobble;animation-name:wobble}@-webkit-keyframes jello{0%,11.1%,to{-webkit-transform:none;transform:none}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}@keyframes jello{0%,11.1%,to{-webkit-transform:none;transform:none}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}.jello{-webkit-animation-name:jello;animation-name:jello;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}.bounceIn{-webkit-animation-name:bounceIn;animation-name:bounceIn}@-webkit-keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}to{-webkit-transform:none;transform:none}}.bounceInDown{-webkit-animation-name:bounceInDown;animation-name:bounceInDown}@-webkit-keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}to{-webkit-transform:none;transform:none}}.bounceInLeft{-webkit-animation-name:bounceInLeft;animation-name:bounceInLeft}@-webkit-keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:none;transform:none}}@keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:none;transform:none}}.bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}.bounceOut{-webkit-animation-name:bounceOut;animation-name:bounceOut}@-webkit-keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.bounceOutDown{-webkit-animation-name:bounceOutDown;animation-name:bounceOutDown}@-webkit-keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.bounceOutLeft{-webkit-animation-name:bounceOutLeft;animation-name:bounceOutLeft}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.bounceOutUp{-webkit-animation-name:bounceOutUp;animation-name:bounceOutUp}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInDown{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInDownBig{-webkit-animation-name:fadeInDownBig;animation-name:fadeInDownBig}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInRightBig{-webkit-animation-name:fadeInRightBig;animation-name:fadeInRightBig}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInUp{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:none;transform:none}}.fadeInUpBig{-webkit-animation-name:fadeInUpBig;animation-name:fadeInUpBig}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}@-webkit-keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.fadeOutDownBig{-webkit-animation-name:fadeOutDownBig;animation-name:fadeOutDownBig}@-webkit-keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.fadeOutLeftBig{-webkit-animation-name:fadeOutLeftBig;animation-name:fadeOutLeftBig}@-webkit-keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.fadeOutRightBig{-webkit-animation-name:fadeOutRightBig;animation-name:fadeOutRightBig}@-webkit-keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.fadeOutUpBig{-webkit-animation-name:fadeOutUpBig;animation-name:fadeOutUpBig}@-webkit-keyframes flip{0%{-webkit-transform:perspective(400px) rotateY(-1turn);transform:perspective(400px) rotateY(-1turn)}0%,40%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-190deg);transform:perspective(400px) translateZ(150px) rotateY(-190deg)}50%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-170deg);transform:perspective(400px) translateZ(150px) rotateY(-170deg)}50%,80%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95)}to{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}@keyframes flip{0%{-webkit-transform:perspective(400px) rotateY(-1turn);transform:perspective(400px) rotateY(-1turn)}0%,40%{-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-190deg);transform:perspective(400px) translateZ(150px) rotateY(-190deg)}50%{-webkit-transform:perspective(400px) translateZ(150px) rotateY(-170deg);transform:perspective(400px) translateZ(150px) rotateY(-170deg)}50%,80%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95)}to{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}.animated.flip{-webkit-backface-visibility:visible;backface-visibility:visible;-webkit-animation-name:flip;animation-name:flip}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg)}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg)}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.flipInX{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInX;animation-name:flipInX}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg)}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}0%,40%{-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg)}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.flipInY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInY;animation-name:flipInY}@-webkit-keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}@keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}.flipOutX{-webkit-animation-name:flipOutX;animation-name:flipOutX;-webkit-backface-visibility:visible!important;backface-visibility:visible!important}@-webkit-keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}@keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}.flipOutY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipOutY;animation-name:flipOutY}@-webkit-keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg)}60%,80%{opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:none;transform:none;opacity:1}}@keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg)}60%,80%{opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:none;transform:none;opacity:1}}.lightSpeedIn{-webkit-animation-name:lightSpeedIn;animation-name:lightSpeedIn;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedOut{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}@keyframes lightSpeedOut{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}.lightSpeedOut{-webkit-animation-name:lightSpeedOut;animation-name:lightSpeedOut;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes rotateIn{0%{transform-origin:center;-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateIn{0%{transform-origin:center;-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}.rotateIn{-webkit-animation-name:rotateIn;animation-name:rotateIn}@-webkit-keyframes rotateInDownLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInDownLeft{-webkit-animation-name:rotateInDownLeft;animation-name:rotateInDownLeft}@-webkit-keyframes rotateInDownRight{0%{transform-origin:right bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownRight{0%{transform-origin:right bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInDownRight{-webkit-animation-name:rotateInDownRight;animation-name:rotateInDownRight}@-webkit-keyframes rotateInUpLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpLeft{0%{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInUpLeft{-webkit-animation-name:rotateInUpLeft;animation-name:rotateInUpLeft}@-webkit-keyframes rotateInUpRight{0%{transform-origin:right bottom;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpRight{0%{transform-origin:right bottom;-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}.rotateInUpRight{-webkit-animation-name:rotateInUpRight;animation-name:rotateInUpRight}@-webkit-keyframes rotateOut{0%{transform-origin:center;opacity:1}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}@keyframes rotateOut{0%{transform-origin:center;opacity:1}0%,to{-webkit-transform-origin:center}to{transform-origin:center;-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}.rotateOut{-webkit-animation-name:rotateOut;animation-name:rotateOut}@-webkit-keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}@keyframes rotateOutDownLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}.rotateOutDownLeft{-webkit-animation-name:rotateOutDownLeft;animation-name:rotateOutDownLeft}@-webkit-keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutDownRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.rotateOutDownRight{-webkit-animation-name:rotateOutDownRight;animation-name:rotateOutDownRight}@-webkit-keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutUpLeft{0%{transform-origin:left bottom;opacity:1}0%,to{-webkit-transform-origin:left bottom}to{transform-origin:left bottom;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.rotateOutUpLeft{-webkit-animation-name:rotateOutUpLeft;animation-name:rotateOutUpLeft}@-webkit-keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}@keyframes rotateOutUpRight{0%{transform-origin:right bottom;opacity:1}0%,to{-webkit-transform-origin:right bottom}to{transform-origin:right bottom;-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}.rotateOutUpRight{-webkit-animation-name:rotateOutUpRight;animation-name:rotateOutUpRight}@-webkit-keyframes hinge{0%{transform-origin:top left}0%,20%,60%{-webkit-transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);transform-origin:top left}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}@keyframes hinge{0%{transform-origin:top left}0%,20%,60%{-webkit-transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);transform-origin:top left}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}.hinge{-webkit-animation-name:hinge;animation-name:hinge}@-webkit-keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:none;transform:none}}@keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:none;transform:none}}.rollIn{-webkit-animation-name:rollIn;animation-name:rollIn}@-webkit-keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}@keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}.rollOut{-webkit-animation-name:rollOut;animation-name:rollOut}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}.zoomIn{-webkit-animation-name:zoomIn;animation-name:zoomIn}@-webkit-keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInDown{-webkit-animation-name:zoomInDown;animation-name:zoomInDown}@-webkit-keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInLeft{-webkit-animation-name:zoomInLeft;animation-name:zoomInLeft}@-webkit-keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInRight{-webkit-animation-name:zoomInRight;animation-name:zoomInRight}@-webkit-keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomInUp{-webkit-animation-name:zoomInUp;animation-name:zoomInUp}@-webkit-keyframes zoomOut{0%{opacity:1}50%{-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%,to{opacity:0}}@keyframes zoomOut{0%{opacity:1}50%{-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%,to{opacity:0}}.zoomOut{-webkit-animation-name:zoomOut;animation-name:zoomOut}@-webkit-keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutDown{-webkit-animation-name:zoomOutDown;animation-name:zoomOutDown}@-webkit-keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}@keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}.zoomOutLeft{-webkit-animation-name:zoomOutLeft;animation-name:zoomOutLeft}@-webkit-keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}@keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}.zoomOutRight{-webkit-animation-name:zoomOutRight;animation-name:zoomOutRight}@-webkit-keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.zoomOutUp{-webkit-animation-name:zoomOutUp;animation-name:zoomOutUp}@-webkit-keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInDown{-webkit-animation-name:slideInDown;animation-name:slideInDown}@-webkit-keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInLeft{-webkit-animation-name:slideInLeft;animation-name:slideInLeft}@-webkit-keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInRight{-webkit-animation-name:slideInRight;animation-name:slideInRight}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}@-webkit-keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.slideOutLeft{-webkit-animation-name:slideOutLeft;animation-name:slideOutLeft}@-webkit-keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.slideOutRight{-webkit-animation-name:slideOutRight;animation-name:slideOutRight}@-webkit-keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.slideOutUp{-webkit-animation-name:slideOutUp;animation-name:slideOutUp}
\ No newline at end of file diff --git a/vendor/gitignore/Actionscript.gitignore b/vendor/gitignore/Actionscript.gitignore new file mode 100644 index 00000000000..11e612e9853 --- /dev/null +++ b/vendor/gitignore/Actionscript.gitignore @@ -0,0 +1,19 @@ +# Build and Release Folders +bin/ +bin-debug/ +bin-release/ +[Oo]bj/ # FlashDevelop obj +[Bb]in/ # FlashDevelop bin + +# Other files and folders +.settings/ + +# Executables +*.swf +*.air +*.ipa +*.apk + +# Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties` +# should NOT be excluded as they contain compiler settings and other important +# information for Eclipse / Flash Builder. diff --git a/vendor/gitignore/Ada.gitignore b/vendor/gitignore/Ada.gitignore new file mode 100644 index 00000000000..b4d703968a4 --- /dev/null +++ b/vendor/gitignore/Ada.gitignore @@ -0,0 +1,5 @@ +# Object file +*.o + +# Ada Library Information +*.ali diff --git a/vendor/gitignore/Agda.gitignore b/vendor/gitignore/Agda.gitignore new file mode 100644 index 00000000000..171a38976c1 --- /dev/null +++ b/vendor/gitignore/Agda.gitignore @@ -0,0 +1 @@ +*.agdai diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore new file mode 100644 index 00000000000..a8368751267 --- /dev/null +++ b/vendor/gitignore/Android.gitignore @@ -0,0 +1,39 @@ +# Built application files +*.apk +*.ap_ + +# Files for the Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# Intellij +*.iml + +# Keystore files +*.jks diff --git a/vendor/gitignore/AppEngine.gitignore b/vendor/gitignore/AppEngine.gitignore new file mode 100644 index 00000000000..62273454531 --- /dev/null +++ b/vendor/gitignore/AppEngine.gitignore @@ -0,0 +1,2 @@ +# Google App Engine generated folder +appengine-generated/ diff --git a/vendor/gitignore/AppceleratorTitanium.gitignore b/vendor/gitignore/AppceleratorTitanium.gitignore new file mode 100644 index 00000000000..3abea559761 --- /dev/null +++ b/vendor/gitignore/AppceleratorTitanium.gitignore @@ -0,0 +1,3 @@ +# Build folder and log file +build/ +build.log diff --git a/vendor/gitignore/ArchLinuxPackages.gitignore b/vendor/gitignore/ArchLinuxPackages.gitignore new file mode 100644 index 00000000000..b73905529f2 --- /dev/null +++ b/vendor/gitignore/ArchLinuxPackages.gitignore @@ -0,0 +1,13 @@ +*.tar +*.tar.* +*.jar +*.exe +*.msi +*.zip +*.tgz +*.log +*.log.* +*.sig + +pkg/ +src/ diff --git a/vendor/gitignore/Autotools.gitignore b/vendor/gitignore/Autotools.gitignore new file mode 100644 index 00000000000..1e9158e2a85 --- /dev/null +++ b/vendor/gitignore/Autotools.gitignore @@ -0,0 +1,18 @@ +# http://www.gnu.org/software/automake + +Makefile.in + +# http://www.gnu.org/software/autoconf + +/autom4te.cache +/autoscan.log +/autoscan-*.log +/aclocal.m4 +/compile +/config.h.in +/configure +/configure.scan +/depcomp +/install-sh +/missing +/stamp-h1 diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore new file mode 100644 index 00000000000..b8bd0267bdf --- /dev/null +++ b/vendor/gitignore/C++.gitignore @@ -0,0 +1,28 @@ +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app diff --git a/vendor/gitignore/C.gitignore b/vendor/gitignore/C.gitignore new file mode 100644 index 00000000000..f805e810e5c --- /dev/null +++ b/vendor/gitignore/C.gitignore @@ -0,0 +1,33 @@ +# Object files +*.o +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su diff --git a/vendor/gitignore/CFWheels.gitignore b/vendor/gitignore/CFWheels.gitignore new file mode 100644 index 00000000000..f2fec34ff89 --- /dev/null +++ b/vendor/gitignore/CFWheels.gitignore @@ -0,0 +1,12 @@ +# unpacked plugin folders +plugins/**/* + +# files directory where uploads go +files + +# DBMigrate plugin: generated SQL +db/sql + +# AssetBundler plugin: generated bundles +javascripts/bundles +stylesheets/bundles diff --git a/vendor/gitignore/CMake.gitignore b/vendor/gitignore/CMake.gitignore new file mode 100644 index 00000000000..b558e9afa6d --- /dev/null +++ b/vendor/gitignore/CMake.gitignore @@ -0,0 +1,6 @@ +CMakeCache.txt +CMakeFiles +CMakeScripts +Makefile +cmake_install.cmake +install_manifest.txt diff --git a/vendor/gitignore/CUDA.gitignore b/vendor/gitignore/CUDA.gitignore new file mode 100644 index 00000000000..cb385db83fe --- /dev/null +++ b/vendor/gitignore/CUDA.gitignore @@ -0,0 +1,6 @@ +*.i +*.ii +*.gpu +*.ptx +*.cubin +*.fatbin diff --git a/vendor/gitignore/CakePHP.gitignore b/vendor/gitignore/CakePHP.gitignore new file mode 100644 index 00000000000..c6597e4eabf --- /dev/null +++ b/vendor/gitignore/CakePHP.gitignore @@ -0,0 +1,25 @@ +# CakePHP 3 + +/vendor/* +/config/app.php + +/tmp/cache/models/* +!/tmp/cache/models/empty +/tmp/cache/persistent/* +!/tmp/cache/persistent/empty +/tmp/cache/views/* +!/tmp/cache/views/empty +/tmp/sessions/* +!/tmp/sessions/empty +/tmp/tests/* +!/tmp/tests/empty + +/logs/* +!/logs/empty + +# CakePHP 2 + +/app/tmp/* +/app/Config/core.php +/app/Config/database.php +/vendors/* diff --git a/vendor/gitignore/ChefCookbook.gitignore b/vendor/gitignore/ChefCookbook.gitignore new file mode 100644 index 00000000000..5ee7b7a9a18 --- /dev/null +++ b/vendor/gitignore/ChefCookbook.gitignore @@ -0,0 +1,9 @@ +.vagrant +/cookbooks + +# Bundler +bin/* +.bundle/* + +.kitchen/ +.kitchen.local.yml diff --git a/vendor/gitignore/Clojure.gitignore b/vendor/gitignore/Clojure.gitignore new file mode 120000 index 00000000000..7657a270c45 --- /dev/null +++ b/vendor/gitignore/Clojure.gitignore @@ -0,0 +1 @@ +Leiningen.gitignore
\ No newline at end of file diff --git a/vendor/gitignore/CodeIgniter.gitignore b/vendor/gitignore/CodeIgniter.gitignore new file mode 100644 index 00000000000..0f77d9e1d17 --- /dev/null +++ b/vendor/gitignore/CodeIgniter.gitignore @@ -0,0 +1,6 @@ +*/config/development +*/logs/log-*.php +!*/logs/index.html +*/cache/* +!*/cache/index.html +!*/cache/.htaccess diff --git a/vendor/gitignore/CommonLisp.gitignore b/vendor/gitignore/CommonLisp.gitignore new file mode 100644 index 00000000000..4806e580b60 --- /dev/null +++ b/vendor/gitignore/CommonLisp.gitignore @@ -0,0 +1,3 @@ +*.FASL +*.fasl +*.lisp-temp diff --git a/vendor/gitignore/Composer.gitignore b/vendor/gitignore/Composer.gitignore new file mode 100644 index 00000000000..c4222678424 --- /dev/null +++ b/vendor/gitignore/Composer.gitignore @@ -0,0 +1,6 @@ +composer.phar +/vendor/ + +# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file +# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file +# composer.lock diff --git a/vendor/gitignore/Concrete5.gitignore b/vendor/gitignore/Concrete5.gitignore new file mode 100644 index 00000000000..1fe53611e5d --- /dev/null +++ b/vendor/gitignore/Concrete5.gitignore @@ -0,0 +1,4 @@ +config/site.php +files/cache/* +files/tmp/* +.htaccess diff --git a/vendor/gitignore/Coq.gitignore b/vendor/gitignore/Coq.gitignore new file mode 100644 index 00000000000..d3083b3a605 --- /dev/null +++ b/vendor/gitignore/Coq.gitignore @@ -0,0 +1,3 @@ +*.vo +*.glob +*.v.d diff --git a/vendor/gitignore/CraftCMS.gitignore b/vendor/gitignore/CraftCMS.gitignore new file mode 100644 index 00000000000..a70d4772c46 --- /dev/null +++ b/vendor/gitignore/CraftCMS.gitignore @@ -0,0 +1,3 @@ +# Craft Storage (cache) [http://buildwithcraft.com/help/craft-storage-gitignore] +/craft/storage/* +!/craft/storage/logo/*
\ No newline at end of file diff --git a/vendor/gitignore/D.gitignore b/vendor/gitignore/D.gitignore new file mode 100644 index 00000000000..b4433f8a512 --- /dev/null +++ b/vendor/gitignore/D.gitignore @@ -0,0 +1,20 @@ +# Compiled Object files +*.o +*.obj + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Compiled Static libraries +*.a +*.lib + +# Executables +*.exe + +# DUB +.dub +docs.json +__dummy.html diff --git a/vendor/gitignore/DM.gitignore b/vendor/gitignore/DM.gitignore new file mode 100644 index 00000000000..ba5abdab836 --- /dev/null +++ b/vendor/gitignore/DM.gitignore @@ -0,0 +1,5 @@ +*.dmb +*.rsc +*.int +*.lk +*.zip diff --git a/vendor/gitignore/Dart.gitignore b/vendor/gitignore/Dart.gitignore new file mode 100644 index 00000000000..7c280441649 --- /dev/null +++ b/vendor/gitignore/Dart.gitignore @@ -0,0 +1,27 @@ +# See https://www.dartlang.org/tools/private-files.html + +# Files and directories created by pub +.buildlog +.packages +.project +.pub/ +build/ +**/packages/ + +# Files created by dart2js +# (Most Dart developers will use pub build to compile Dart, use/modify these +# rules if you intend to use dart2js directly +# Convention is to use extension '.dart.js' for Dart compiled to Javascript to +# differentiate from explicit Javascript files) +*.dart.js +*.part.js +*.js.deps +*.js.map +*.info.json + +# Directory created by dartdoc +doc/api/ + +# Don't commit pubspec lock file +# (Library packages only! Remove pattern if developing an application package) +pubspec.lock diff --git a/vendor/gitignore/Delphi.gitignore b/vendor/gitignore/Delphi.gitignore new file mode 100644 index 00000000000..19864c6bbef --- /dev/null +++ b/vendor/gitignore/Delphi.gitignore @@ -0,0 +1,66 @@ +# Uncomment these types if you want even more clean repository. But be careful. +# It can make harm to an existing project source. Read explanations below. +# +# Resource files are binaries containing manifest, project icon and version info. +# They can not be viewed as text or compared by diff-tools. Consider replacing them with .rc files. +#*.res +# +# Type library file (binary). In old Delphi versions it should be stored. +# Since Delphi 2009 it is produced from .ridl file and can safely be ignored. +#*.tlb +# +# Diagram Portfolio file. Used by the diagram editor up to Delphi 7. +# Uncomment this if you are not using diagrams or use newer Delphi version. +#*.ddp +# +# Visual LiveBindings file. Added in Delphi XE2. +# Uncomment this if you are not using LiveBindings Designer. +#*.vlb +# +# Deployment Manager configuration file for your project. Added in Delphi XE2. +# Uncomment this if it is not mobile development and you do not use remote debug feature. +#*.deployproj +# +# C++ object files produced when C/C++ Output file generation is configured. +# Uncomment this if you are not using external objects (zlib library for example). +#*.obj +# + +# Delphi compiler-generated binaries (safe to delete) +*.exe +*.dll +*.bpl +*.bpi +*.dcp +*.so +*.apk +*.drc +*.map +*.dres +*.rsm +*.tds +*.dcu +*.lib +*.a +*.o +*.ocx + +# Delphi autogenerated files (duplicated info) +*.cfg +*.hpp +*Resource.rc + +# Delphi local files (user-specific info) +*.local +*.identcache +*.projdata +*.tvsconfig +*.dsk + +# Delphi history and backups +__history/ +__recovery/ +*.~* + +# Castalia statistics file (since XE7 Castalia is distributed with Delphi) +*.stat diff --git a/vendor/gitignore/Drupal.gitignore b/vendor/gitignore/Drupal.gitignore new file mode 100644 index 00000000000..0d2fe537f46 --- /dev/null +++ b/vendor/gitignore/Drupal.gitignore @@ -0,0 +1,36 @@ +# Ignore configuration files that may contain sensitive information. +sites/*/*settings*.php + +# Ignore paths that contain generated content. +files/ +sites/*/files +sites/*/private + +# Ignore default text files +robots.txt +/CHANGELOG.txt +/COPYRIGHT.txt +/INSTALL*.txt +/LICENSE.txt +/MAINTAINERS.txt +/UPGRADE.txt +/README.txt +sites/README.txt +sites/all/modules/README.txt +sites/all/themes/README.txt + +# Ignore everything but the "sites" folder ( for non core developer ) +.htaccess +web.config +authorize.php +cron.php +index.php +install.php +update.php +xmlrpc.php +/includes +/misc +/modules +/profiles +/scripts +/themes diff --git a/vendor/gitignore/EPiServer.gitignore b/vendor/gitignore/EPiServer.gitignore new file mode 100644 index 00000000000..97037de743e --- /dev/null +++ b/vendor/gitignore/EPiServer.gitignore @@ -0,0 +1,4 @@ +###################### +## EPiServer Files +###################### +*License.config diff --git a/vendor/gitignore/Eagle.gitignore b/vendor/gitignore/Eagle.gitignore new file mode 100644 index 00000000000..9ced1260266 --- /dev/null +++ b/vendor/gitignore/Eagle.gitignore @@ -0,0 +1,44 @@ +# Ignore list for Eagle, a PCB layout tool + +# Backup files +*.s#? +*.b#? +*.l#? + +# Eagle project file +# It contains a serial number and references to the file structure +# on your computer. +# comment the following line if you want to have your project file included. +eagle.epf + +# Autorouter files +*.pro +*.job + +# CAM files +*.$$$ +*.cmp +*.ly2 +*.l15 +*.sol +*.plc +*.stc +*.sts +*.crc +*.crs + +*.dri +*.drl +*.gpi +*.pls + +*.drd +*.drd.* + +*.info + +*.eps + +# file locks introduced since 7.x +*.lck + diff --git a/vendor/gitignore/Elisp.gitignore b/vendor/gitignore/Elisp.gitignore new file mode 100644 index 00000000000..9b4291b7fe8 --- /dev/null +++ b/vendor/gitignore/Elisp.gitignore @@ -0,0 +1,5 @@ +# Compiled +*.elc + +# Packaging +.cask diff --git a/vendor/gitignore/Elixir.gitignore b/vendor/gitignore/Elixir.gitignore new file mode 100644 index 00000000000..755b605549d --- /dev/null +++ b/vendor/gitignore/Elixir.gitignore @@ -0,0 +1,5 @@ +/_build +/cover +/deps +erl_crash.dump +*.ez diff --git a/vendor/gitignore/Elm.gitignore b/vendor/gitignore/Elm.gitignore new file mode 100644 index 00000000000..a594364e2c0 --- /dev/null +++ b/vendor/gitignore/Elm.gitignore @@ -0,0 +1,4 @@ +# elm-package generated files +elm-stuff/ +# elm-repl generated files +repl-temp-* diff --git a/vendor/gitignore/Erlang.gitignore b/vendor/gitignore/Erlang.gitignore new file mode 100644 index 00000000000..8e46d5a07f8 --- /dev/null +++ b/vendor/gitignore/Erlang.gitignore @@ -0,0 +1,10 @@ +.eunit +deps +*.o +*.beam +*.plt +erl_crash.dump +ebin +rel/example_project +.concrete/DEV_MODE +.rebar diff --git a/vendor/gitignore/ExpressionEngine.gitignore b/vendor/gitignore/ExpressionEngine.gitignore new file mode 100644 index 00000000000..314e4df123a --- /dev/null +++ b/vendor/gitignore/ExpressionEngine.gitignore @@ -0,0 +1,19 @@ +.DS_Store + +# Images +images/avatars/ +images/captchas/ +images/smileys/ +images/member_photos/ +images/signature_attachments/ +images/pm_attachments/ + +# For security do not publish the following files +system/expressionengine/config/database.php +system/expressionengine/config/config.php + +# Caches +sized/ +thumbs/ +_thumbs/ +*/expressionengine/cache/* diff --git a/vendor/gitignore/ExtJs.gitignore b/vendor/gitignore/ExtJs.gitignore new file mode 100644 index 00000000000..5ffc21546ec --- /dev/null +++ b/vendor/gitignore/ExtJs.gitignore @@ -0,0 +1,4 @@ +.architect +bootstrap.json +build/ +ext/ diff --git a/vendor/gitignore/Fancy.gitignore b/vendor/gitignore/Fancy.gitignore new file mode 100644 index 00000000000..70d6e631e55 --- /dev/null +++ b/vendor/gitignore/Fancy.gitignore @@ -0,0 +1,2 @@ +*.rbc +*.fyc diff --git a/vendor/gitignore/Finale.gitignore b/vendor/gitignore/Finale.gitignore new file mode 100644 index 00000000000..7ef08e0c343 --- /dev/null +++ b/vendor/gitignore/Finale.gitignore @@ -0,0 +1,13 @@ +*.bak +*.db +*.avi +*.pdf +*.ps +*.mid +*.midi +*.mp3 +*.aif +*.wav +# Some versions of Finale have a bug and randomly save extra copies of +# the music source as "<Filename> copy.mus" +*copy.mus diff --git a/vendor/gitignore/ForceDotCom.gitignore b/vendor/gitignore/ForceDotCom.gitignore new file mode 100644 index 00000000000..3933cd4dd50 --- /dev/null +++ b/vendor/gitignore/ForceDotCom.gitignore @@ -0,0 +1,4 @@ +.project +.settings +salesforce.schema +Referenced Packages diff --git a/vendor/gitignore/Fortran.gitignore b/vendor/gitignore/Fortran.gitignore new file mode 120000 index 00000000000..5daba98a3e6 --- /dev/null +++ b/vendor/gitignore/Fortran.gitignore @@ -0,0 +1 @@ +C++.gitignore
\ No newline at end of file diff --git a/vendor/gitignore/FuelPHP.gitignore b/vendor/gitignore/FuelPHP.gitignore new file mode 100644 index 00000000000..d69f71f4338 --- /dev/null +++ b/vendor/gitignore/FuelPHP.gitignore @@ -0,0 +1,21 @@ +# the composer package lock file and install directory +# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file +# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file +# /composer.lock +/fuel/vendor + +# the fuelphp document +/docs/ + +# you may install these packages with `oil package`. +# http://fuelphp.com/docs/packages/oil/package.html +# /fuel/packages/auth/ +# /fuel/packages/email/ +# /fuel/packages/oil/ +# /fuel/packages/orm/ +# /fuel/packages/parser/ + +# dynamically generated files +/fuel/app/logs/*/*/* +/fuel/app/cache/*/* +/fuel/app/config/crypt.php diff --git a/vendor/gitignore/GWT.gitignore b/vendor/gitignore/GWT.gitignore new file mode 100644 index 00000000000..07704e54bbc --- /dev/null +++ b/vendor/gitignore/GWT.gitignore @@ -0,0 +1,28 @@ +*.class + +# Package Files # +*.jar +*.war + +# gwt caches and compiled units # +war/gwt_bree/ +gwt-unitCache/ + +# boilerplate generated classes # +.apt_generated/ + +# more caches and things from deploy # +war/WEB-INF/deploy/ +war/WEB-INF/classes/ + +#compilation logs +.gwt/ + +#caching for already compiled files +gwt-unitCache/ + +#gwt junit compilation files +www-test/ + +#old GWT (1.5) created this dir +.gwt-tmp/ diff --git a/vendor/gitignore/Gcov.gitignore b/vendor/gitignore/Gcov.gitignore new file mode 100644 index 00000000000..a6451430e17 --- /dev/null +++ b/vendor/gitignore/Gcov.gitignore @@ -0,0 +1,5 @@ +# gcc coverage testing tool files + +*.gcno +*.gcda +*.gcov diff --git a/vendor/gitignore/GitBook.gitignore b/vendor/gitignore/GitBook.gitignore new file mode 100644 index 00000000000..4cb12d8db77 --- /dev/null +++ b/vendor/gitignore/GitBook.gitignore @@ -0,0 +1,16 @@ +# Node rules: +## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +## Dependency directory +## Commenting this out is preferred by some people, see +## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git +node_modules + +# Book build output +_book + +# eBook build output +*.epub +*.mobi +*.pdf diff --git a/vendor/gitignore/Global/Anjuta.gitignore b/vendor/gitignore/Global/Anjuta.gitignore new file mode 100644 index 00000000000..20dd42c53e6 --- /dev/null +++ b/vendor/gitignore/Global/Anjuta.gitignore @@ -0,0 +1,3 @@ +# Local configuration folder and symbol database +/.anjuta/ +/.anjuta_sym_db.db diff --git a/vendor/gitignore/Global/Archives.gitignore b/vendor/gitignore/Global/Archives.gitignore new file mode 100644 index 00000000000..e9eda68baf2 --- /dev/null +++ b/vendor/gitignore/Global/Archives.gitignore @@ -0,0 +1,27 @@ +# It's better to unpack these files and commit the raw source because +# git has its own built in compression methods. +*.7z +*.jar +*.rar +*.zip +*.gz +*.bzip +*.bz2 +*.xz +*.lzma +*.cab + +#packing-only formats +*.iso +*.tar + +#package management formats +*.dmg +*.xpi +*.gem +*.egg +*.deb +*.rpm +*.msi +*.msm +*.msp diff --git a/vendor/gitignore/Global/BricxCC.gitignore b/vendor/gitignore/Global/BricxCC.gitignore new file mode 100644 index 00000000000..c1d16a46c98 --- /dev/null +++ b/vendor/gitignore/Global/BricxCC.gitignore @@ -0,0 +1,4 @@ +# Bricx Command Center IDE +# http://bricxcc.sourceforge.net +*.bak +*.sym diff --git a/vendor/gitignore/Global/CVS.gitignore b/vendor/gitignore/Global/CVS.gitignore new file mode 100644 index 00000000000..1695352e146 --- /dev/null +++ b/vendor/gitignore/Global/CVS.gitignore @@ -0,0 +1,4 @@ +/CVS/* +**/CVS/* +.cvsignore +*/.cvsignore diff --git a/vendor/gitignore/Global/Calabash.gitignore b/vendor/gitignore/Global/Calabash.gitignore new file mode 100644 index 00000000000..8a75b329dcd --- /dev/null +++ b/vendor/gitignore/Global/Calabash.gitignore @@ -0,0 +1,10 @@ +# Calabash / Cucumber +rerun/ +reports/ +screenshots/ +screenshot*.png +test-servers/ + +# bundler +.bundle +vendor diff --git a/vendor/gitignore/Global/Cloud9.gitignore b/vendor/gitignore/Global/Cloud9.gitignore new file mode 100644 index 00000000000..3f4384df508 --- /dev/null +++ b/vendor/gitignore/Global/Cloud9.gitignore @@ -0,0 +1,3 @@ +# Cloud9 IDE - http://c9.io +.c9revisions +.c9 diff --git a/vendor/gitignore/Global/CodeKit.gitignore b/vendor/gitignore/Global/CodeKit.gitignore new file mode 100644 index 00000000000..bd9e67fcca2 --- /dev/null +++ b/vendor/gitignore/Global/CodeKit.gitignore @@ -0,0 +1,3 @@ +# General CodeKit files to ignore +config.codekit +/min diff --git a/vendor/gitignore/Global/DartEditor.gitignore b/vendor/gitignore/Global/DartEditor.gitignore new file mode 100644 index 00000000000..948920b420e --- /dev/null +++ b/vendor/gitignore/Global/DartEditor.gitignore @@ -0,0 +1,2 @@ +.project +.buildlog diff --git a/vendor/gitignore/Global/Dreamweaver.gitignore b/vendor/gitignore/Global/Dreamweaver.gitignore new file mode 100644 index 00000000000..0621a3d53b5 --- /dev/null +++ b/vendor/gitignore/Global/Dreamweaver.gitignore @@ -0,0 +1,7 @@ +# DW Dreamweaver added files +_notes +_compareTemp +configs/ +dwsync.xml +dw_php_codehinting.config +*.mno diff --git a/vendor/gitignore/Global/Dropbox.gitignore b/vendor/gitignore/Global/Dropbox.gitignore new file mode 100644 index 00000000000..40f4a469d25 --- /dev/null +++ b/vendor/gitignore/Global/Dropbox.gitignore @@ -0,0 +1,4 @@ +# Dropbox settings and caches +.dropbox +.dropbox.attr +.dropbox.cache diff --git a/vendor/gitignore/Global/Eclipse.gitignore b/vendor/gitignore/Global/Eclipse.gitignore new file mode 100644 index 00000000000..31c9fb31167 --- /dev/null +++ b/vendor/gitignore/Global/Eclipse.gitignore @@ -0,0 +1,51 @@ + +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# Eclipse Core +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ diff --git a/vendor/gitignore/Global/EiffelStudio.gitignore b/vendor/gitignore/Global/EiffelStudio.gitignore new file mode 100644 index 00000000000..f41b4f70216 --- /dev/null +++ b/vendor/gitignore/Global/EiffelStudio.gitignore @@ -0,0 +1,2 @@ +# The compilation directory +EIFGENs diff --git a/vendor/gitignore/Global/Emacs.gitignore b/vendor/gitignore/Global/Emacs.gitignore new file mode 100644 index 00000000000..0c96c9ad060 --- /dev/null +++ b/vendor/gitignore/Global/Emacs.gitignore @@ -0,0 +1,42 @@ +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile
\ No newline at end of file diff --git a/vendor/gitignore/Global/Ensime.gitignore b/vendor/gitignore/Global/Ensime.gitignore new file mode 100644 index 00000000000..f2daebb9f4b --- /dev/null +++ b/vendor/gitignore/Global/Ensime.gitignore @@ -0,0 +1,4 @@ +# Ensime specific +.ensime +.ensime_cache/ +.ensime_lucene/ diff --git a/vendor/gitignore/Global/Espresso.gitignore b/vendor/gitignore/Global/Espresso.gitignore new file mode 100644 index 00000000000..1234530b5b3 --- /dev/null +++ b/vendor/gitignore/Global/Espresso.gitignore @@ -0,0 +1 @@ +*.esproj diff --git a/vendor/gitignore/Global/FlexBuilder.gitignore b/vendor/gitignore/Global/FlexBuilder.gitignore new file mode 100644 index 00000000000..bbbfb91d9eb --- /dev/null +++ b/vendor/gitignore/Global/FlexBuilder.gitignore @@ -0,0 +1,3 @@ +bin/ +bin-debug/ +bin-release/ diff --git a/vendor/gitignore/Global/GPG.gitignore b/vendor/gitignore/Global/GPG.gitignore new file mode 100644 index 00000000000..7740a01538c --- /dev/null +++ b/vendor/gitignore/Global/GPG.gitignore @@ -0,0 +1,2 @@ +secring.* + diff --git a/vendor/gitignore/Global/IPythonNotebook.gitignore b/vendor/gitignore/Global/IPythonNotebook.gitignore new file mode 100644 index 00000000000..27c13510bf5 --- /dev/null +++ b/vendor/gitignore/Global/IPythonNotebook.gitignore @@ -0,0 +1,2 @@ +# Temporary data +.ipynb_checkpoints/ diff --git a/vendor/gitignore/Global/JDeveloper.gitignore b/vendor/gitignore/Global/JDeveloper.gitignore new file mode 100644 index 00000000000..5bba6f37733 --- /dev/null +++ b/vendor/gitignore/Global/JDeveloper.gitignore @@ -0,0 +1,13 @@ +# default application storage directory used by the IDE Performance Cache feature +.data/ + +# used for ADF styles caching +temp/ + +# default output directories +classes/ +deploy/ +javadoc/ + +# lock file, a part of Oracle Credential Store Framework +cwallet.sso.lck
\ No newline at end of file diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore new file mode 100644 index 00000000000..ea83a5eb620 --- /dev/null +++ b/vendor/gitignore/Global/JetBrains.gitignore @@ -0,0 +1,44 @@ +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/workspace.xml +.idea/tasks.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml + +# Sensitive or high-churn files: +.idea/dataSources.ids +.idea/dataSources.xml +.idea/dataSources.local.xml +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# Gradle: +.idea/gradle.xml +.idea/libraries + +# Mongo Explorer plugin: +.idea/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties diff --git a/vendor/gitignore/Global/KDevelop4.gitignore b/vendor/gitignore/Global/KDevelop4.gitignore new file mode 100644 index 00000000000..7ac57b1add4 --- /dev/null +++ b/vendor/gitignore/Global/KDevelop4.gitignore @@ -0,0 +1,2 @@ +*.kdev4 +.kdev4/ diff --git a/vendor/gitignore/Global/Kate.gitignore b/vendor/gitignore/Global/Kate.gitignore new file mode 100644 index 00000000000..7ff06ce5390 --- /dev/null +++ b/vendor/gitignore/Global/Kate.gitignore @@ -0,0 +1,3 @@ +# Swap Files # +.*.kate-swp +.swp.* diff --git a/vendor/gitignore/Global/Lazarus.gitignore b/vendor/gitignore/Global/Lazarus.gitignore new file mode 100644 index 00000000000..b32943f1c6e --- /dev/null +++ b/vendor/gitignore/Global/Lazarus.gitignore @@ -0,0 +1,30 @@ +# Lazarus compiler-generated binaries (safe to delete) +*.exe +*.dll +*.so +*.dylib +*.lrs +*.res +*.compiled +*.dbg +*.ppu +*.o +*.or +*.a + +# Lazarus autogenerated files (duplicated info) +*.rst +*.rsj +*.lrt + +# Lazarus local files (user-specific info) +*.lps + +# Lazarus backups and unit output folders. +# These can be changed by user in Lazarus/project options. +backup/ +*.bak +lib/ + +# Application bundle for Mac OS +*.app/ diff --git a/vendor/gitignore/Global/LibreOffice.gitignore b/vendor/gitignore/Global/LibreOffice.gitignore new file mode 100644 index 00000000000..586beac91d3 --- /dev/null +++ b/vendor/gitignore/Global/LibreOffice.gitignore @@ -0,0 +1,2 @@ +# LibreOffice locks +.~lock.*# diff --git a/vendor/gitignore/Global/Linux.gitignore b/vendor/gitignore/Global/Linux.gitignore new file mode 100644 index 00000000000..cc9586893b6 --- /dev/null +++ b/vendor/gitignore/Global/Linux.gitignore @@ -0,0 +1,10 @@ +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* diff --git a/vendor/gitignore/Global/LyX.gitignore b/vendor/gitignore/Global/LyX.gitignore new file mode 100644 index 00000000000..8efe0195cf3 --- /dev/null +++ b/vendor/gitignore/Global/LyX.gitignore @@ -0,0 +1,4 @@ +# Ignore LyX backup and autosave files +# http://www.lyx.org/ +*.lyx~ +*.lyx# diff --git a/vendor/gitignore/Global/Matlab.gitignore b/vendor/gitignore/Global/Matlab.gitignore new file mode 100644 index 00000000000..32a5ad4c777 --- /dev/null +++ b/vendor/gitignore/Global/Matlab.gitignore @@ -0,0 +1,19 @@ +##--------------------------------------------------- +## Remove autosaves generated by the Matlab editor +## We have git for backups! +##--------------------------------------------------- + +# Windows default autosave extension +*.asv + +# OSX / *nix default autosave extension +*.m~ + +# Compiled MEX binaries (all platforms) +*.mex* + +# Simulink Code Generation +slprj/ + +# Session info +octave-workspace diff --git a/vendor/gitignore/Global/Mercurial.gitignore b/vendor/gitignore/Global/Mercurial.gitignore new file mode 100644 index 00000000000..e65d1137988 --- /dev/null +++ b/vendor/gitignore/Global/Mercurial.gitignore @@ -0,0 +1,6 @@ +.hg/ +.hgignore +.hgsigs +.hgsub +.hgsubstate +.hgtags diff --git a/vendor/gitignore/Global/MicrosoftOffice.gitignore b/vendor/gitignore/Global/MicrosoftOffice.gitignore new file mode 100644 index 00000000000..cb891745660 --- /dev/null +++ b/vendor/gitignore/Global/MicrosoftOffice.gitignore @@ -0,0 +1,16 @@ +*.tmp + +# Word temporary +~$*.doc* + +# Excel temporary +~$*.xls* + +# Excel Backup File +*.xlk + +# PowerPoint temporary +~$*.ppt* + +# Visio autosave temporary files +*.~vsdx diff --git a/vendor/gitignore/Global/ModelSim.gitignore b/vendor/gitignore/Global/ModelSim.gitignore new file mode 100644 index 00000000000..46592b86430 --- /dev/null +++ b/vendor/gitignore/Global/ModelSim.gitignore @@ -0,0 +1,23 @@ +# ignore ModelSim generated files and directories (temp files and so on) +[_@]* + +# ignore compilation output of ModelSim +*.mti +*.dat +*.dbs +*.psm +*.bak +*.cmp +*.jpg +*.html +*.bsf + +# ignore simulation output of ModelSim +wlf* +*.wlf +*.vstf +*.ucdb +cov*/ +transcript* +sc_dpiheader.h +vsim.dbg diff --git a/vendor/gitignore/Global/Momentics.gitignore b/vendor/gitignore/Global/Momentics.gitignore new file mode 100644 index 00000000000..b14db2d8645 --- /dev/null +++ b/vendor/gitignore/Global/Momentics.gitignore @@ -0,0 +1,8 @@ +# Built files +x86/ +arm/ +arm-p/ +translations/*.qm + +# IDE settings +.settings/ diff --git a/vendor/gitignore/Global/MonoDevelop.gitignore b/vendor/gitignore/Global/MonoDevelop.gitignore new file mode 100644 index 00000000000..ef38d06b08f --- /dev/null +++ b/vendor/gitignore/Global/MonoDevelop.gitignore @@ -0,0 +1,8 @@ +#User Specific +*.userprefs +*.usertasks + +#Mono Project Files +*.pidb +*.resources +test-results/ diff --git a/vendor/gitignore/Global/NetBeans.gitignore b/vendor/gitignore/Global/NetBeans.gitignore new file mode 100644 index 00000000000..520d91ff584 --- /dev/null +++ b/vendor/gitignore/Global/NetBeans.gitignore @@ -0,0 +1,7 @@ +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +nbactions.xml +.nb-gradle/ diff --git a/vendor/gitignore/Global/Ninja.gitignore b/vendor/gitignore/Global/Ninja.gitignore new file mode 100644 index 00000000000..50e58f24cc9 --- /dev/null +++ b/vendor/gitignore/Global/Ninja.gitignore @@ -0,0 +1,2 @@ +.ninja_deps +.ninja_log diff --git a/vendor/gitignore/Global/NotepadPP.gitignore b/vendor/gitignore/Global/NotepadPP.gitignore new file mode 100644 index 00000000000..8fbda83a2c9 --- /dev/null +++ b/vendor/gitignore/Global/NotepadPP.gitignore @@ -0,0 +1,2 @@ +# Notepad++ backups #
+*.bak
diff --git a/vendor/gitignore/Global/OSX.gitignore b/vendor/gitignore/Global/OSX.gitignore new file mode 100644 index 00000000000..660b31353e8 --- /dev/null +++ b/vendor/gitignore/Global/OSX.gitignore @@ -0,0 +1,24 @@ +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon
+ +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk diff --git a/vendor/gitignore/Global/Otto.gitignore b/vendor/gitignore/Global/Otto.gitignore new file mode 100644 index 00000000000..5aa263f9db0 --- /dev/null +++ b/vendor/gitignore/Global/Otto.gitignore @@ -0,0 +1 @@ +.otto/ diff --git a/vendor/gitignore/Global/Redcar.gitignore b/vendor/gitignore/Global/Redcar.gitignore new file mode 100644 index 00000000000..b4a9d1d68e3 --- /dev/null +++ b/vendor/gitignore/Global/Redcar.gitignore @@ -0,0 +1 @@ +.redcar diff --git a/vendor/gitignore/Global/Redis.gitignore b/vendor/gitignore/Global/Redis.gitignore new file mode 100644 index 00000000000..57c1c230f92 --- /dev/null +++ b/vendor/gitignore/Global/Redis.gitignore @@ -0,0 +1,3 @@ +# Ignore redis binary dump (dump.rdb) files + +*.rdb diff --git a/vendor/gitignore/Global/SBT.gitignore b/vendor/gitignore/Global/SBT.gitignore new file mode 100644 index 00000000000..970d897c75c --- /dev/null +++ b/vendor/gitignore/Global/SBT.gitignore @@ -0,0 +1,9 @@ +# Simple Build Tool +# http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control + +target/ +lib_managed/ +src_managed/ +project/boot/ +.history +.cache diff --git a/vendor/gitignore/Global/SVN.gitignore b/vendor/gitignore/Global/SVN.gitignore new file mode 100644 index 00000000000..1b53ace613f --- /dev/null +++ b/vendor/gitignore/Global/SVN.gitignore @@ -0,0 +1 @@ +.svn/ diff --git a/vendor/gitignore/Global/SlickEdit.gitignore b/vendor/gitignore/Global/SlickEdit.gitignore new file mode 100644 index 00000000000..f30b8da457c --- /dev/null +++ b/vendor/gitignore/Global/SlickEdit.gitignore @@ -0,0 +1,11 @@ +# SlickEdit workspace and project files are ignored by default because +# typically they are considered to be developer-specific and not part of a +# project. +*.vpw +*.vpj + +# SlickEdit workspace history and tag files always contain user-specific +# data so they should not be stored in a repository. +*.vpwhistu +*.vpwhist +*.vtg diff --git a/vendor/gitignore/Global/SublimeText.gitignore b/vendor/gitignore/Global/SublimeText.gitignore new file mode 100644 index 00000000000..1d4e6137591 --- /dev/null +++ b/vendor/gitignore/Global/SublimeText.gitignore @@ -0,0 +1,14 @@ +# cache files for sublime text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# workspace files are user-specific +*.sublime-workspace + +# project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using SublimeText +# *.sublime-project + +# sftp configuration file +sftp-config.json diff --git a/vendor/gitignore/Global/SynopsysVCS.gitignore b/vendor/gitignore/Global/SynopsysVCS.gitignore new file mode 100644 index 00000000000..eed2432fb78 --- /dev/null +++ b/vendor/gitignore/Global/SynopsysVCS.gitignore @@ -0,0 +1,36 @@ +# Waveform formats +*.vcd +*.vpd +*.evcd +*.fsdb + +# Default name of the simulation executable. A different name can be +# specified with this switch (the associated daidir database name is +# also taken from here): -o <path>/<filename> +simv + +# Generated for Verilog and VHDL top configs +simv.daidir/ +simv.db.dir/ + +# Infrastructure necessary to co-simulate SystemC models with +# Verilog/VHDL models. An alternate directory may be specified with this +# switch: -Mdir=<directory_path> +csrc/ + +# Log file - the following switch allows to specify the file that will be +# used to write all messages from simulation: -l <filename> +*.log + +# Coverage results (generated with urg) and database location. The +# following switch can also be used: urg -dir <coverage_directory>.vdb +simv.vdb/ +urgReport/ + +# DVE and UCLI related files. +DVEfiles/ +ucli.key + +# When the design is elaborated for DirectC, the following file is created +# with declarations for C/C++ functions. +vc_hdrs.h diff --git a/vendor/gitignore/Global/Tags.gitignore b/vendor/gitignore/Global/Tags.gitignore new file mode 100644 index 00000000000..c0318165a27 --- /dev/null +++ b/vendor/gitignore/Global/Tags.gitignore @@ -0,0 +1,16 @@ +# Ignore tags created by etags, ctags, gtags (GNU global) and cscope +TAGS +.TAGS +!TAGS/ +tags +.tags +!tags/ +gtags.files +GTAGS +GRTAGS +GPATH +cscope.files +cscope.out +cscope.in.out +cscope.po.out + diff --git a/vendor/gitignore/Global/TextMate.gitignore b/vendor/gitignore/Global/TextMate.gitignore new file mode 100644 index 00000000000..41e8d07a940 --- /dev/null +++ b/vendor/gitignore/Global/TextMate.gitignore @@ -0,0 +1,3 @@ +*.tmproj +*.tmproject +tmtags diff --git a/vendor/gitignore/Global/TortoiseGit.gitignore b/vendor/gitignore/Global/TortoiseGit.gitignore new file mode 100644 index 00000000000..db89590a629 --- /dev/null +++ b/vendor/gitignore/Global/TortoiseGit.gitignore @@ -0,0 +1,2 @@ +# Project-level settings +/.tgitconfig diff --git a/vendor/gitignore/Global/Vagrant.gitignore b/vendor/gitignore/Global/Vagrant.gitignore new file mode 100644 index 00000000000..a977916f658 --- /dev/null +++ b/vendor/gitignore/Global/Vagrant.gitignore @@ -0,0 +1 @@ +.vagrant/ diff --git a/vendor/gitignore/Global/Vim.gitignore b/vendor/gitignore/Global/Vim.gitignore new file mode 100644 index 00000000000..bdc04a0b529 --- /dev/null +++ b/vendor/gitignore/Global/Vim.gitignore @@ -0,0 +1,10 @@ +# swap +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags diff --git a/vendor/gitignore/Global/VirtualEnv.gitignore b/vendor/gitignore/Global/VirtualEnv.gitignore new file mode 100644 index 00000000000..b2c22f2af7f --- /dev/null +++ b/vendor/gitignore/Global/VirtualEnv.gitignore @@ -0,0 +1,12 @@ +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +.Python +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +.venv +pip-selfcheck.json diff --git a/vendor/gitignore/Global/VisualStudioCode.gitignore b/vendor/gitignore/Global/VisualStudioCode.gitignore new file mode 100644 index 00000000000..faa18382a3c --- /dev/null +++ b/vendor/gitignore/Global/VisualStudioCode.gitignore @@ -0,0 +1,2 @@ +.vscode + diff --git a/vendor/gitignore/Global/WebMethods.gitignore b/vendor/gitignore/Global/WebMethods.gitignore new file mode 100644 index 00000000000..b383c25ca3c --- /dev/null +++ b/vendor/gitignore/Global/WebMethods.gitignore @@ -0,0 +1,14 @@ +**/IntegrationServer/datastore/ +**/IntegrationServer/db/ +**/IntegrationServer/DocumentStore/ +**/IntegrationServer/lib/ +**/IntegrationServer/logs/ +**/IntegrationServer/replicate/ +**/IntegrationServer/sdk/ +**/IntegrationServer/support/ +**/IntegrationServer/update/ +**/IntegrationServer/userFtpRoot/ +**/IntegrationServer/web/ +**/IntegrationServer/WmRepository4/ +**/IntegrationServer/XAStore/ +**/IntegrationServer/packages/Wm*/ diff --git a/vendor/gitignore/Global/Windows.gitignore b/vendor/gitignore/Global/Windows.gitignore new file mode 100644 index 00000000000..a0d31452b0e --- /dev/null +++ b/vendor/gitignore/Global/Windows.gitignore @@ -0,0 +1,18 @@ +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk diff --git a/vendor/gitignore/Global/Xcode.gitignore b/vendor/gitignore/Global/Xcode.gitignore new file mode 100644 index 00000000000..37de8bb4793 --- /dev/null +++ b/vendor/gitignore/Global/Xcode.gitignore @@ -0,0 +1,23 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xccheckout +*.xcscmblueprint diff --git a/vendor/gitignore/Global/XilinxISE.gitignore b/vendor/gitignore/Global/XilinxISE.gitignore new file mode 100644 index 00000000000..4475f843da9 --- /dev/null +++ b/vendor/gitignore/Global/XilinxISE.gitignore @@ -0,0 +1,67 @@ +# intermediate build files +*.bgn +*.bit +*.bld +*.cmd_log +*.drc +*.ll +*.lso +*.msd +*.msk +*.ncd +*.ngc +*.ngd +*.ngr +*.pad +*.par +*.pcf +*.prj +*.ptwx +*.rbb +*.rbd +*.stx +*.syr +*.twr +*.twx +*.unroutes +*.ut +*.xpi +*.xst +*_bitgen.xwbt +*_envsettings.html +*_map.map +*_map.mrp +*_map.ngm +*_map.xrpt +*_ngdbuild.xrpt +*_pad.csv +*_pad.txt +*_par.xrpt +*_summary.html +*_summary.xml +*_usage.xml +*_xst.xrpt + +# iMPACT generated files +_impactbatch.log +impact.xsl +impact_impact.xwbt +ise_impact.cmd +webtalk_impact.xml + +# Core Generator generated files +xaw2verilog.log + +# project-wide generated files +*.gise +par_usage_statistics.html +usage_statistics_webtalk.html +webtalk.log +webtalk_pn.xml + +# generated folders +iseconfig/ +xlnx_auto_0_xdb/ +xst/ +_ngo/ +_xmsgs/ diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore new file mode 100644 index 00000000000..daf913b1b34 --- /dev/null +++ b/vendor/gitignore/Go.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/gitignore/Gradle.gitignore b/vendor/gitignore/Gradle.gitignore new file mode 100644 index 00000000000..77617a15c38 --- /dev/null +++ b/vendor/gitignore/Gradle.gitignore @@ -0,0 +1,14 @@ +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties diff --git a/vendor/gitignore/Grails.gitignore b/vendor/gitignore/Grails.gitignore new file mode 100644 index 00000000000..9185f14c37c --- /dev/null +++ b/vendor/gitignore/Grails.gitignore @@ -0,0 +1,33 @@ +# .gitignore for Grails 1.2 and 1.3 +# Although this should work for most versions of grails, it is +# suggested that you use the "grails integrate-with --git" command +# to generate your .gitignore file. + +# web application files +/web-app/WEB-INF/classes + +# default HSQL database files for production mode +/prodDb.* + +# general HSQL database files +*Db.properties +*Db.script + +# logs +/stacktrace.log +/test/reports +/logs + +# project release file +/*.war + +# plugin release files +/*.zip +/plugin.xml + +# older plugin install locations +/plugins +/web-app/plugins + +# "temporary" build files +/target diff --git a/vendor/gitignore/Haskell.gitignore b/vendor/gitignore/Haskell.gitignore new file mode 100644 index 00000000000..096abdd90b3 --- /dev/null +++ b/vendor/gitignore/Haskell.gitignore @@ -0,0 +1,18 @@ +dist +dist-* +cabal-dev +*.o +*.hi +*.chi +*.chs.h +*.dyn_o +*.dyn_hi +.hpc +.hsenv +.cabal-sandbox/ +cabal.sandbox.config +*.prof +*.aux +*.hp +*.eventlog +.stack-work/ diff --git a/vendor/gitignore/IGORPro.gitignore b/vendor/gitignore/IGORPro.gitignore new file mode 100644 index 00000000000..c62be650036 --- /dev/null +++ b/vendor/gitignore/IGORPro.gitignore @@ -0,0 +1,5 @@ +# Avoid including Experiment files: they can be created and edited locally to test the ipf files +*.pxp +*.pxt +*.uxp +*.uxt diff --git a/vendor/gitignore/Idris.gitignore b/vendor/gitignore/Idris.gitignore new file mode 100644 index 00000000000..c28bc7cc675 --- /dev/null +++ b/vendor/gitignore/Idris.gitignore @@ -0,0 +1,2 @@ +*.ibc +*.o diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore new file mode 100644 index 00000000000..32858aad3c3 --- /dev/null +++ b/vendor/gitignore/Java.gitignore @@ -0,0 +1,12 @@ +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* diff --git a/vendor/gitignore/Jboss.gitignore b/vendor/gitignore/Jboss.gitignore new file mode 100644 index 00000000000..75d1731ed97 --- /dev/null +++ b/vendor/gitignore/Jboss.gitignore @@ -0,0 +1,19 @@ +jboss/server/all/deploy/project.ext +jboss/server/default/deploy/project.ext +jboss/server/minimal/deploy/project.ext +jboss/server/all/log/*.log +jboss/server/all/tmp/**/* +jboss/server/all/data/**/* +jboss/server/all/work/**/* +jboss/server/default/log/*.log +jboss/server/default/tmp/**/* +jboss/server/default/data/**/* +jboss/server/default/work/**/* +jboss/server/minimal/log/*.log +jboss/server/minimal/tmp/**/* +jboss/server/minimal/data/**/* +jboss/server/minimal/work/**/* + +# deployed package files # + +*.DEPLOYED diff --git a/vendor/gitignore/Jekyll.gitignore b/vendor/gitignore/Jekyll.gitignore new file mode 100644 index 00000000000..5c91b60c063 --- /dev/null +++ b/vendor/gitignore/Jekyll.gitignore @@ -0,0 +1,3 @@ +_site/ +.sass-cache/ +.jekyll-metadata diff --git a/vendor/gitignore/Joomla.gitignore b/vendor/gitignore/Joomla.gitignore new file mode 100644 index 00000000000..0d7a0de298f --- /dev/null +++ b/vendor/gitignore/Joomla.gitignore @@ -0,0 +1,546 @@ +/.gitignore +/.htaccess +/administrator/cache/* +/administrator/components/com_admin/* +/administrator/components/com_ajax/* +/administrator/components/com_tags/* +/administrator/components/com_banners/* +/administrator/components/com_cache/* +/administrator/components/com_postinstall/* +/administrator/components/com_joomlaupdate/* +/administrator/components/com_contenthistory/* +/administrator/components/com_categories/* +/administrator/components/com_checkin/* +/administrator/components/com_config/* +/administrator/components/com_contact/* +/administrator/components/com_content/* +/administrator/components/com_cpanel/* +/administrator/components/com_finder/* +/administrator/components/com_installer/* +/administrator/components/com_languages/* +/administrator/components/com_login/* +/administrator/components/com_media/* +/administrator/components/com_menus/* +/administrator/components/com_messages/* +/administrator/components/com_modules/* +/administrator/components/com_newsfeeds/* +/administrator/components/com_plugins/* +/administrator/components/com_redirect/* +/administrator/components/com_search/* +/administrator/components/com_templates/* +/administrator/components/com_users/* +/administrator/components/com_weblinks/* +/administrator/components/index.html +/administrator/help/* +/administrator/includes/* +/administrator/language/en-GB/en-GB.com_ajax.ini +/administrator/language/en-GB/en-GB.com_ajax.sys.ini +/administrator/language/en-GB/en-GB.com_contenthistory.ini +/administrator/language/en-GB/en-GB.com_contenthistory.sys.ini +/administrator/language/en-GB/en-GB.com_joomlaupdate.ini +/administrator/language/en-GB/en-GB.com_joomlaupdate.sys.ini +/administrator/language/en-GB/en-GB.com_postinstall.ini +/administrator/language/en-GB/en-GB.com_postinstall.sys.ini +/administrator/language/en-GB/en-GB.com_sitemapjen.sys.ini +/administrator/language/en-GB/en-GB.com_tags.ini +/administrator/language/en-GB/en-GB.com_tags.sys.ini +/administrator/language/en-GB/en-GB.mod_stats_admin.ini +/administrator/language/en-GB/en-GB.mod_stats_admin.sys.ini +/administrator/language/en-GB/en-GB.plg_authentication_cookie.ini +/administrator/language/en-GB/en-GB.plg_authentication_cookie.sys.ini +/administrator/language/en-GB/en-GB.plg_content_contact.ini +/administrator/language/en-GB/en-GB.plg_content_contact.sys.ini +/administrator/language/en-GB/en-GB.plg_content_finder.ini +/administrator/language/en-GB/en-GB.plg_content_finder.sys.ini +/administrator/language/en-GB/en-GB.plg_finder_categories.ini +/administrator/language/en-GB/en-GB.plg_finder_categories.sys.ini +/administrator/language/en-GB/en-GB.plg_finder_contacts.ini +/administrator/language/en-GB/en-GB.plg_finder_contacts.sys.ini +/administrator/language/en-GB/en-GB.plg_finder_content.ini +/administrator/language/en-GB/en-GB.plg_finder_content.sys.ini +/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.sys.ini +/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.ini +/administrator/language/en-GB/en-GB.plg_finder_tags.ini +/administrator/language/en-GB/en-GB.plg_finder_tags.sys.ini +/administrator/language/en-GB/en-GB.plg_finder_weblinks.ini +/administrator/language/en-GB/en-GB.plg_finder_weblinks.sys.ini +/administrator/language/en-GB/en-GB.plg_installer_webinstaller.ini +/administrator/language/en-GB/en-GB.plg_installer_webinstaller.sys.ini +/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.ini +/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.sys.ini +/administrator/language/en-GB/en-GB.plg_search_tags.ini +/administrator/language/en-GB/en-GB.plg_search_tags.sys.ini +/administrator/language/en-GB/en-GB.plg_system_languagecode.ini +/administrator/language/en-GB/en-GB.plg_system_languagecode.sys.ini +/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.ini +/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.sys.ini +/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.ini +/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.sys.ini +/administrator/language/en-GB/en-GB.tpl_isis.ini +/administrator/language/en-GB/en-GB.tpl_isis.sys.ini +/administrator/language/en-GB/install.xml +/administrator/language/en-GB/en-GB.com_admin.ini +/administrator/language/en-GB/en-GB.com_admin.sys.ini +/administrator/language/en-GB/en-GB.com_banners.ini +/administrator/language/en-GB/en-GB.com_banners.sys.ini +/administrator/language/en-GB/en-GB.com_cache.ini +/administrator/language/en-GB/en-GB.com_cache.sys.ini +/administrator/language/en-GB/en-GB.com_categories.ini +/administrator/language/en-GB/en-GB.com_categories.sys.ini +/administrator/language/en-GB/en-GB.com_checkin.ini +/administrator/language/en-GB/en-GB.com_checkin.sys.ini +/administrator/language/en-GB/en-GB.com_config.ini +/administrator/language/en-GB/en-GB.com_config.sys.ini +/administrator/language/en-GB/en-GB.com_contact.ini +/administrator/language/en-GB/en-GB.com_contact.sys.ini +/administrator/language/en-GB/en-GB.com_content.ini +/administrator/language/en-GB/en-GB.com_content.sys.ini +/administrator/language/en-GB/en-GB.com_cpanel.ini +/administrator/language/en-GB/en-GB.com_cpanel.sys.ini +/administrator/language/en-GB/en-GB.com_finder.ini +/administrator/language/en-GB/en-GB.com_finder.sys.ini +/administrator/language/en-GB/en-GB.com_installer.ini +/administrator/language/en-GB/en-GB.com_installer.sys.ini +/administrator/language/en-GB/en-GB.com_languages.ini +/administrator/language/en-GB/en-GB.com_languages.sys.ini +/administrator/language/en-GB/en-GB.com_login.ini +/administrator/language/en-GB/en-GB.com_login.sys.ini +/administrator/language/en-GB/en-GB.com_mailto.sys.ini +/administrator/language/en-GB/en-GB.com_media.ini +/administrator/language/en-GB/en-GB.com_media.sys.ini +/administrator/language/en-GB/en-GB.com_menus.ini +/administrator/language/en-GB/en-GB.com_menus.sys.ini +/administrator/language/en-GB/en-GB.com_messages.ini +/administrator/language/en-GB/en-GB.com_messages.sys.ini +/administrator/language/en-GB/en-GB.com_modules.ini +/administrator/language/en-GB/en-GB.com_modules.sys.ini +/administrator/language/en-GB/en-GB.com_newsfeeds.ini +/administrator/language/en-GB/en-GB.com_newsfeeds.sys.ini +/administrator/language/en-GB/en-GB.com_plugins.ini +/administrator/language/en-GB/en-GB.com_plugins.sys.ini +/administrator/language/en-GB/en-GB.com_redirect.ini +/administrator/language/en-GB/en-GB.com_redirect.sys.ini +/administrator/language/en-GB/en-GB.com_search.ini +/administrator/language/en-GB/en-GB.com_search.sys.ini +/administrator/language/en-GB/en-GB.com_templates.ini +/administrator/language/en-GB/en-GB.com_templates.sys.ini +/administrator/language/en-GB/en-GB.com_users.ini +/administrator/language/en-GB/en-GB.com_users.sys.ini +/administrator/language/en-GB/en-GB.com_weblinks.ini +/administrator/language/en-GB/en-GB.com_weblinks.sys.ini +/administrator/language/en-GB/en-GB.com_wrapper.ini +/administrator/language/en-GB/en-GB.com_wrapper.sys.ini +/administrator/language/en-GB/en-GB.ini +/administrator/language/en-GB/en-GB.lib_joomla.ini +/administrator/language/en-GB/en-GB.localise.php +/administrator/language/en-GB/en-GB.mod_custom.ini +/administrator/language/en-GB/en-GB.mod_custom.sys.ini +/administrator/language/en-GB/en-GB.mod_feed.ini +/administrator/language/en-GB/en-GB.mod_feed.sys.ini +/administrator/language/en-GB/en-GB.mod_latest.ini +/administrator/language/en-GB/en-GB.mod_latest.sys.ini +/administrator/language/en-GB/en-GB.mod_logged.ini +/administrator/language/en-GB/en-GB.mod_logged.sys.ini +/administrator/language/en-GB/en-GB.mod_login.ini +/administrator/language/en-GB/en-GB.mod_login.sys.ini +/administrator/language/en-GB/en-GB.mod_menu.ini +/administrator/language/en-GB/en-GB.mod_menu.sys.ini +/administrator/language/en-GB/en-GB.mod_multilangstatus.ini +/administrator/language/en-GB/en-GB.mod_multilangstatus.sys.ini +/administrator/language/en-GB/en-GB.mod_online.ini +/administrator/language/en-GB/en-GB.mod_online.sys.ini +/administrator/language/en-GB/en-GB.mod_popular.ini +/administrator/language/en-GB/en-GB.mod_popular.sys.ini +/administrator/language/en-GB/en-GB.mod_quickicon.ini +/administrator/language/en-GB/en-GB.mod_quickicon.sys.ini +/administrator/language/en-GB/en-GB.mod_status.ini +/administrator/language/en-GB/en-GB.mod_status.sys.ini +/administrator/language/en-GB/en-GB.mod_submenu.ini +/administrator/language/en-GB/en-GB.mod_submenu.sys.ini +/administrator/language/en-GB/en-GB.mod_title.ini +/administrator/language/en-GB/en-GB.mod_title.sys.ini +/administrator/language/en-GB/en-GB.mod_toolbar.ini +/administrator/language/en-GB/en-GB.mod_toolbar.sys.ini +/administrator/language/en-GB/en-GB.mod_unread.ini +/administrator/language/en-GB/en-GB.mod_unread.sys.ini +/administrator/language/en-GB/en-GB.mod_version.ini +/administrator/language/en-GB/en-GB.mod_version.sys.ini +/administrator/language/en-GB/en-GB.plg_authentication_example.ini +/administrator/language/en-GB/en-GB.plg_authentication_example.sys.ini +/administrator/language/en-GB/en-GB.plg_authentication_gmail.ini +/administrator/language/en-GB/en-GB.plg_authentication_gmail.sys.ini +/administrator/language/en-GB/en-GB.plg_authentication_joomla.ini +/administrator/language/en-GB/en-GB.plg_authentication_joomla.sys.ini +/administrator/language/en-GB/en-GB.plg_authentication_ldap.ini +/administrator/language/en-GB/en-GB.plg_authentication_ldap.sys.ini +/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.ini +/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.sys.ini +/administrator/language/en-GB/en-GB.plg_content_emailcloak.ini +/administrator/language/en-GB/en-GB.plg_content_emailcloak.sys.ini +/administrator/language/en-GB/en-GB.plg_content_geshi.ini +/administrator/language/en-GB/en-GB.plg_content_geshi.sys.ini +/administrator/language/en-GB/en-GB.plg_content_joomla.ini +/administrator/language/en-GB/en-GB.plg_content_joomla.sys.ini +/administrator/language/en-GB/en-GB.plg_content_loadmodule.ini +/administrator/language/en-GB/en-GB.plg_content_loadmodule.sys.ini +/administrator/language/en-GB/en-GB.plg_content_pagebreak.ini +/administrator/language/en-GB/en-GB.plg_content_pagebreak.sys.ini +/administrator/language/en-GB/en-GB.plg_content_pagenavigation.ini +/administrator/language/en-GB/en-GB.plg_content_pagenavigation.sys.ini +/administrator/language/en-GB/en-GB.plg_content_vote.ini +/administrator/language/en-GB/en-GB.plg_content_vote.sys.ini +/administrator/language/en-GB/en-GB.plg_editors_codemirror.ini +/administrator/language/en-GB/en-GB.plg_editors_codemirror.sys.ini +/administrator/language/en-GB/en-GB.plg_editors_none.ini +/administrator/language/en-GB/en-GB.plg_editors_none.sys.ini +/administrator/language/en-GB/en-GB.plg_editors_tinymce.ini +/administrator/language/en-GB/en-GB.plg_editors_tinymce.sys.ini +/administrator/language/en-GB/en-GB.plg_editors-xtd_article.ini +/administrator/language/en-GB/en-GB.plg_editors-xtd_article.sys.ini +/administrator/language/en-GB/en-GB.plg_editors-xtd_image.ini +/administrator/language/en-GB/en-GB.plg_editors-xtd_image.sys.ini +/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.ini +/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.sys.ini +/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.ini +/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.sys.ini +/administrator/language/en-GB/en-GB.plg_extension_joomla.ini +/administrator/language/en-GB/en-GB.plg_extension_joomla.sys.ini +/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.ini +/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.sys.ini +/administrator/language/en-GB/en-GB.plg_search_categories.ini +/administrator/language/en-GB/en-GB.plg_search_categories.sys.ini +/administrator/language/en-GB/en-GB.plg_search_contacts.ini +/administrator/language/en-GB/en-GB.plg_search_contacts.sys.ini +/administrator/language/en-GB/en-GB.plg_search_content.ini +/administrator/language/en-GB/en-GB.plg_search_content.sys.ini +/administrator/language/en-GB/en-GB.plg_search_newsfeeds.ini +/administrator/language/en-GB/en-GB.plg_search_newsfeeds.sys.ini +/administrator/language/en-GB/en-GB.plg_search_weblinks.ini +/administrator/language/en-GB/en-GB.plg_search_weblinks.sys.ini +/administrator/language/en-GB/en-GB.plg_system_cache.ini +/administrator/language/en-GB/en-GB.plg_system_cache.sys.ini +/administrator/language/en-GB/en-GB.plg_system_debug.ini +/administrator/language/en-GB/en-GB.plg_system_debug.sys.ini +/administrator/language/en-GB/en-GB.plg_system_highlight.ini +/administrator/language/en-GB/en-GB.plg_system_highlight.sys.ini +/administrator/language/en-GB/en-GB.plg_system_languagefilter.ini +/administrator/language/en-GB/en-GB.plg_system_languagefilter.sys.ini +/administrator/language/en-GB/en-GB.plg_system_log.ini +/administrator/language/en-GB/en-GB.plg_system_logout.ini +/administrator/language/en-GB/en-GB.plg_system_logout.sys.ini +/administrator/language/en-GB/en-GB.plg_system_log.sys.ini +/administrator/language/en-GB/en-GB.plg_system_p3p.ini +/administrator/language/en-GB/en-GB.plg_system_p3p.sys.ini +/administrator/language/en-GB/en-GB.plg_system_redirect.ini +/administrator/language/en-GB/en-GB.plg_system_redirect.sys.ini +/administrator/language/en-GB/en-GB.plg_system_remember.ini +/administrator/language/en-GB/en-GB.plg_system_remember.sys.ini +/administrator/language/en-GB/en-GB.plg_system_sef.ini +/administrator/language/en-GB/en-GB.plg_system_sef.sys.ini +/administrator/language/en-GB/en-GB.plg_user_contactcreator.ini +/administrator/language/en-GB/en-GB.plg_user_contactcreator.sys.ini +/administrator/language/en-GB/en-GB.plg_user_joomla.ini +/administrator/language/en-GB/en-GB.plg_user_joomla.sys.ini +/administrator/language/en-GB/en-GB.plg_user_profile.ini +/administrator/language/en-GB/en-GB.plg_user_profile.sys.ini +/administrator/language/en-GB/en-GB.tpl_bluestork.ini +/administrator/language/en-GB/en-GB.tpl_bluestork.sys.ini +/administrator/language/en-GB/en-GB.tpl_hathor.ini +/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini +/administrator/language/en-GB/en-GB.xml +/administrator/language/en-GB/index.html +/administrator/language/overrides/* +/administrator/language/index.html +/administrator/manifests/* +/administrator/modules/mod_custom/* +/administrator/modules/mod_feed/* +/administrator/modules/mod_latest/* +/administrator/modules/mod_logged/* +/administrator/modules/mod_login/* +/administrator/modules/mod_menu/* +/administrator/modules/mod_multilangstatus/* +/administrator/modules/mod_online/* +/administrator/modules/mod_popular/* +/administrator/modules/mod_quickicon/* +/administrator/modules/mod_status/* +/administrator/modules/mod_submenu/* +/administrator/modules/mod_title/* +/administrator/modules/mod_toolbar/* +/administrator/modules/mod_unread/* +/administrator/modules/mod_version/* +/administrator/modules/mod_stats_admin/* +/administrator/modules/index.html +/administrator/templates/bluestork/* +/administrator/templates/isis/* +/administrator/templates/hathor/* +/administrator/templates/system/* +/administrator/templates/index.html +/administrator/index.php +/cache/* +/bin/* +/cli/* +/components/com_banners/* +/components/com_ajax/* +/components/com_config/* +/components/com_contenthistory/* +/components/com_tags/* +/components/com_contact/* +/components/com_content/* +/components/com_finder/* +/components/com_mailto/* +/components/com_media/* +/components/com_newsfeeds/* +/components/com_search/* +/components/com_users/* +/components/com_weblinks/* +/components/com_wrapper/* +/components/index.html +/images/banners/* +/images/headers/* +/images/sampledata/* +/images/joomla* +/images/index.html +/images/powered_by.png +/includes/* +/installation/* +/language/en-GB/en-GB.com_ajax.ini +/language/en-GB/en-GB.com_config.ini +/language/en-GB/en-GB.com_contact.ini +/language/en-GB/en-GB.com_finder.ini +/language/en-GB/en-GB.com_tags.ini +/language/en-GB/en-GB.finder_cli.ini +/language/en-GB/en-GB.lib_fof.sys.ini +/language/en-GB/en-GB.lib_fof.ini +/language/en-GB/en-GB.com_content.ini +/language/en-GB/en-GB.lib_idna_convert.sys.ini +/language/en-GB/en-GB.com_mailto.ini +/language/en-GB/en-GB.lib_joomla.sys.ini +/language/en-GB/en-GB.lib_phpass.sys.ini +/language/en-GB/en-GB.lib_phpmailer.sys.ini +/language/en-GB/en-GB.lib_phputf8.sys.ini +/language/en-GB/en-GB.lib_simplepie.sys.ini +/language/en-GB/en-GB.com_media.ini +/language/en-GB/en-GB.mod_finder.ini +/language/en-GB/en-GB.com_messages.ini +/language/en-GB/en-GB.mod_tags_popular.ini +/language/en-GB/en-GB.mod_tags_popular.sys.ini +/language/en-GB/en-GB.mod_tags_similar.ini +/language/en-GB/en-GB.mod_tags_similar.sys.ini +/language/en-GB/en-GB.mod_finder.sys.ini +/language/en-GB/en-GB.tpl_beez3.ini +/language/en-GB/en-GB.tpl_beez3.sys.ini +/language/en-GB/en-GB.com_newsfeeds.ini +/language/en-GB/en-GB.tpl_protostar.ini +/language/en-GB/en-GB.tpl_protostar.sys.ini +/language/en-GB/en-GB.com_search.ini +/language/en-GB/en-GB.com_users.ini +/language/en-GB/en-GB.com_weblinks.ini +/language/en-GB/en-GB.com_wrapper.ini +/language/en-GB/en-GB.files_joomla.sys.ini +/language/en-GB/en-GB.ini +/language/en-GB/en-GB.lib_joomla.ini +/language/en-GB/en-GB.localise.php +/language/en-GB/en-GB.mod_articles_archive.ini +/language/en-GB/en-GB.mod_articles_archive.sys.ini +/language/en-GB/en-GB.mod_articles_categories.ini +/language/en-GB/en-GB.mod_articles_categories.sys.ini +/language/en-GB/en-GB.mod_articles_category.ini +/language/en-GB/en-GB.mod_articles_category.sys.ini +/language/en-GB/en-GB.mod_articles_latest.ini +/language/en-GB/en-GB.mod_articles_latest.sys.ini +/language/en-GB/en-GB.mod_articles_news.ini +/language/en-GB/en-GB.mod_articles_news.sys.ini +/language/en-GB/en-GB.mod_articles_popular.ini +/language/en-GB/en-GB.mod_articles_popular.sys.ini +/language/en-GB/en-GB.mod_banners.ini +/language/en-GB/en-GB.mod_banners.sys.ini +/language/en-GB/en-GB.mod_breadcrumbs.ini +/language/en-GB/en-GB.mod_breadcrumbs.sys.ini +/language/en-GB/en-GB.mod_custom.ini +/language/en-GB/en-GB.mod_custom.sys.ini +/language/en-GB/en-GB.mod_feed.ini +/language/en-GB/en-GB.mod_feed.sys.ini +/language/en-GB/en-GB.mod_footer.ini +/language/en-GB/en-GB.mod_footer.sys.ini +/language/en-GB/en-GB.mod_languages.ini +/language/en-GB/en-GB.mod_languages.sys.ini +/language/en-GB/en-GB.mod_login.ini +/language/en-GB/en-GB.mod_login.sys.ini +/language/en-GB/en-GB.mod_menu.ini +/language/en-GB/en-GB.mod_menu.sys.ini +/language/en-GB/en-GB.mod_random_image.ini +/language/en-GB/en-GB.mod_random_image.sys.ini +/language/en-GB/en-GB.mod_related_items.ini +/language/en-GB/en-GB.mod_related_items.sys.ini +/language/en-GB/en-GB.mod_search.ini +/language/en-GB/en-GB.mod_search.sys.ini +/language/en-GB/en-GB.mod_stats.ini +/language/en-GB/en-GB.mod_stats.sys.ini +/language/en-GB/en-GB.mod_syndicate.ini +/language/en-GB/en-GB.mod_syndicate.sys.ini +/language/en-GB/en-GB.mod_users_latest.ini +/language/en-GB/en-GB.mod_users_latest.sys.ini +/language/en-GB/en-GB.mod_weblinks.ini +/language/en-GB/en-GB.mod_weblinks.sys.ini +/language/en-GB/en-GB.mod_whosonline.ini +/language/en-GB/en-GB.mod_whosonline.sys.ini +/language/en-GB/en-GB.mod_wrapper.ini +/language/en-GB/en-GB.mod_wrapper.sys.ini +/language/en-GB/en-GB.tpl_atomic.ini +/language/en-GB/en-GB.tpl_atomic.sys.ini +/language/en-GB/en-GB.tpl_beez_20.ini +/language/en-GB/en-GB.tpl_beez_20.sys.ini +/language/en-GB/en-GB.tpl_beez5.ini +/language/en-GB/en-GB.tpl_beez5.sys.ini +/language/en-GB/en-GB.xml +/language/en-GB/index.html +/language/en-GB/install.xml +/language/overrides/* +/language/index.html +/layouts/joomla/* +/layouts/libraries/* +/layouts/plugins/* +/layouts/index.html +/libraries/cms.php +/libraries/cms/* +/libraries/fof/* +/libraries/idna_convert/* +/libraries/joomla/* +/libraries/legacy/* +/libraries/phpass/* +/libraries/phpmailer/* +/libraries/phputf8/* +/libraries/simplepie/* +/libraries/vendor/* +/libraries/classmap.php +/libraries/import.legacy.php +/libraries/index.html +/libraries/import.php +/libraries/loader.php +/libraries/platform.php +/logs/* +/media/cms/* +/media/com_contenthistory/* +/media/com_finder/* +/media/com_joomlaupdate/* +/media/com_wrapper/* +/media/contacts/* +/media/editors/* +/media/jui/* +/media/mailto/* +/media/media/* +/media/mod_languages/* +/media/overrider/* +/media/plg_quickicon_extensionupdate/* +/media/plg_quickicon_joomlaupdate/* +/media/plg_system_highlight/* +/media/system/* +/media/index.html +/modules/mod_articles_archive/* +/modules/mod_articles_categories/* +/modules/mod_articles_category/* +/modules/mod_articles_latest/* +/modules/mod_articles_news/* +/modules/mod_articles_popular/* +/modules/mod_banners/* +/modules/mod_breadcrumbs/* +/modules/mod_custom/* +/modules/mod_feed/* +/modules/mod_finder/* +/modules/mod_footer/* +/modules/mod_languages/* +/modules/mod_login/* +/modules/mod_menu/* +/modules/mod_random_image/* +/modules/mod_related_items/* +/modules/mod_search/* +/modules/mod_stats/* +/modules/mod_syndicate/* +/modules/mod_tags_popular/* +/modules/mod_tags_similar/* +/modules/mod_users_latest/* +/modules/mod_weblinks/* +/modules/mod_whosonline/* +/modules/mod_wrapper/* +/modules/index.html +/plugins/authentication/example/* +/plugins/authentication/gmail/* +/plugins/authentication/joomla/* +/plugins/authentication/ldap/* +/plugins/authentication/cookie/* +/plugins/authentication/index.html +/plugins/captcha/recaptcha/* +/plugins/captcha/index.html +/plugins/content/emailcloak/* +/plugins/content/example/* +/plugins/content/finder/* +/plugins/content/geshi/* +/plugins/content/joomla/* +/plugins/content/loadmodule/* +/plugins/content/pagebreak/* +/plugins/content/pagenavigation/* +/plugins/content/vote/* +/plugins/content/contact/* +/plugins/content/index.html +/plugins/editors/codemirror/* +/plugins/editors/none/* +/plugins/editors/tinymce/* +/plugins/editors/index.html +/plugins/editors-xtd/article/* +/plugins/editors-xtd/image/* +/plugins/editors-xtd/pagebreak/* +/plugins/editors-xtd/readmore/* +/plugins/editors-xtd/index.html +/plugins/extension/example/* +/plugins/extension/joomla/* +/plugins/extension/index.html +/plugins/finder/index.html +/plugins/finder/categories/* +/plugins/finder/contacts/* +/plugins/finder/content/* +/plugins/finder/newsfeeds/* +/plugins/finder/tags/* +/plugins/finder/weblinks/* +/plugins/installer/* +/plugins/quickicon/extensionupdate/* +/plugins/quickicon/joomlaupdate/* +/plugins/quickicon/index.html +/plugins/search/categories/* +/plugins/search/contacts/* +/plugins/search/content/* +/plugins/search/newsfeeds/* +/plugins/search/weblinks/* +/plugins/search/tags/* +/plugins/search/index.html +/plugins/system/cache/* +/plugins/system/debug/* +/plugins/system/highlight/* +/plugins/system/languagecode/* +/plugins/system/languagefilter/* +/plugins/system/log/* +/plugins/system/logout/* +/plugins/system/p3p/* +/plugins/system/redirect/* +/plugins/system/remember/* +/plugins/system/sef/* +/plugins/system/index.html +/plugins/twofactorauth/* +/plugins/user/contactcreator/* +/plugins/user/example/* +/plugins/user/joomla/* +/plugins/user/profile/* +/plugins/user/index.html +/plugins/index.html +/templates/atomic/* +/templates/beez3/* +/templates/beez_20/* +/templates/beez5/* +/templates/protostar/* +/templates/system/* +/templates/index.html +/tmp/* +/configuration.php +/index.php +/joomla.xml +/*.txt +/robots.txt.dist diff --git a/vendor/gitignore/KiCad.gitignore b/vendor/gitignore/KiCad.gitignore new file mode 100644 index 00000000000..606ed1c7b4d --- /dev/null +++ b/vendor/gitignore/KiCad.gitignore @@ -0,0 +1,20 @@ +# For PCBs designed using KiCad: http://www.kicad-pcb.org/ + +# Temporary files +*.000 +*.bak +*.bck +*.kicad_pcb-bak +*~ +_autosave-* +*.tmp + +# Netlist files (exported from Eeschema) +*.net + +# Autorouter files (exported from Pcbnew) +.dsn + +# Exported BOM files +*.xml +*.csv diff --git a/vendor/gitignore/Kohana.gitignore b/vendor/gitignore/Kohana.gitignore new file mode 100644 index 00000000000..8b2ab01a800 --- /dev/null +++ b/vendor/gitignore/Kohana.gitignore @@ -0,0 +1,2 @@ +application/cache/* +application/logs/* diff --git a/vendor/gitignore/LabVIEW.gitignore b/vendor/gitignore/LabVIEW.gitignore new file mode 100644 index 00000000000..122450865cf --- /dev/null +++ b/vendor/gitignore/LabVIEW.gitignore @@ -0,0 +1,16 @@ +# Libraries +*.lvlibp +*.llb + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe + +# Metadata +*.aliases +*.lvlps diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore new file mode 100644 index 00000000000..c491fa2bc6f --- /dev/null +++ b/vendor/gitignore/Laravel.gitignore @@ -0,0 +1,16 @@ +vendor/ +node_modules/ + +# Laravel 4 specific +bootstrap/compiled.php +app/storage/ + +# Laravel 5 & Lumen specific +bootstrap/cache/ +storage/ +.env.*.php +.env.php +.env + +# Rocketeer PHP task runner and deployment package. https://github.com/rocketeers/rocketeer +.rocketeer/ diff --git a/vendor/gitignore/Leiningen.gitignore b/vendor/gitignore/Leiningen.gitignore new file mode 100644 index 00000000000..47fed6c20d9 --- /dev/null +++ b/vendor/gitignore/Leiningen.gitignore @@ -0,0 +1,12 @@ +pom.xml +pom.xml.asc +*jar +/lib/ +/classes/ +/target/ +/checkouts/ +.lein-deps-sum +.lein-repl-history +.lein-plugins/ +.lein-failures +.nrepl-port diff --git a/vendor/gitignore/LemonStand.gitignore b/vendor/gitignore/LemonStand.gitignore new file mode 100644 index 00000000000..c7d94ad34b0 --- /dev/null +++ b/vendor/gitignore/LemonStand.gitignore @@ -0,0 +1,21 @@ +boot.php +index.php +install.php +/config/* +!/config/config.php +/controllers/* +/init/* +/logs/* +/phproad/* +/temp/* +/uploaded/* +/installer_files/* +/modules/backend/* +/modules/blog/* +/modules/cms/* +/modules/core/* +/modules/session/* +/modules/shop/* +/modules/system/* +/modules/users/* +# add content_*.php if you don't want erase client changes to content diff --git a/vendor/gitignore/Lilypond.gitignore b/vendor/gitignore/Lilypond.gitignore new file mode 100644 index 00000000000..513e6edd9c4 --- /dev/null +++ b/vendor/gitignore/Lilypond.gitignore @@ -0,0 +1,6 @@ +*.pdf +*.ps +*.midi +*.mid +*.log +*~ diff --git a/vendor/gitignore/Lithium.gitignore b/vendor/gitignore/Lithium.gitignore new file mode 100644 index 00000000000..7b22568ea89 --- /dev/null +++ b/vendor/gitignore/Lithium.gitignore @@ -0,0 +1,2 @@ +libraries/* +resources/tmp/* diff --git a/vendor/gitignore/Lua.gitignore b/vendor/gitignore/Lua.gitignore new file mode 100644 index 00000000000..6fd0a376dec --- /dev/null +++ b/vendor/gitignore/Lua.gitignore @@ -0,0 +1,41 @@ +# Compiled Lua sources +luac.out + +# luarocks build files +*.src.rock +*.zip +*.tar.gz + +# Object files +*.o +*.os +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo +*.def +*.exp + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + diff --git a/vendor/gitignore/Magento.gitignore b/vendor/gitignore/Magento.gitignore new file mode 100644 index 00000000000..195c9b7a029 --- /dev/null +++ b/vendor/gitignore/Magento.gitignore @@ -0,0 +1,104 @@ +.htaccess.sample +.modgit/ +.modman/ +app/code/community/Phoenix/Moneybookers/ +app/code/community/Cm/RedisSession/ +app/code/core/ +app/design/adminhtml/default/default/ +app/design/frontend/base/ +app/design/frontend/rwd/ +app/design/frontend/default/blank/ +app/design/frontend/default/default/ +app/design/frontend/default/iphone/ +app/design/frontend/default/modern/ +app/design/frontend/enterprise/default +app/design/install/ +app/etc/modules/Enterprise_* +app/etc/modules/Mage_*.xml +app/etc/modules/Phoenix_Moneybookers.xml +app/etc/modules/Cm_RedisSession.xml +app/etc/applied.patches.list +app/etc/config.xml +app/etc/enterprise.xml +app/etc/local.xml.additional +app/etc/local.xml.template +app/etc/local.xml +app/.htaccess +app/bootstrap.php +app/locale/en_US/ +app/Mage.php +/cron.php +cron.sh +dev/.htaccess +dev/tests/functional/ +downloader/ +errors/ +favicon.ico +/get.php +includes/ +/index.php +index.php.sample +/install.php +js/blank.html +js/calendar/ +js/enterprise/ +js/extjs/ +js/firebug/ +js/flash/ +js/index.php +js/jscolor/ +js/lib/ +js/mage/ +js/prototype/ +js/scriptaculous/ +js/spacer.gif +js/tiny_mce/ +js/varien/ +lib/3Dsecure/ +lib/Apache/ +lib/flex/ +lib/googlecheckout/ +lib/.htaccess +lib/LinLibertineFont/ +lib/Mage/ +lib/PEAR/ +lib/Pelago/ +lib/phpseclib/ +lib/Varien/ +lib/Zend/ +lib/Cm/ +lib/Credis/ +lib/Magento/ +LICENSE_AFL.txt +LICENSE.html +LICENSE.txt +LICENSE_EE* +/mage +media/ +/api.php +nbproject/ +pear +pear/ +php.ini.sample +pkginfo/ +RELEASE_NOTES.txt +shell/.htaccess +shell/abstract.php +shell/compiler.php +shell/indexer.php +shell/log.php +sitemap.xml +skin/adminhtml/default/default/ +skin/adminhtml/default/enterprise +skin/frontend/base/ +skin/frontend/rwd/ +skin/frontend/default/blank/ +skin/frontend/default/blue/ +skin/frontend/default/default/ +skin/frontend/default/french/ +skin/frontend/default/german/ +skin/frontend/default/iphone/ +skin/frontend/default/modern/ +skin/frontend/enterprise +skin/install/ +var/ diff --git a/vendor/gitignore/Maven.gitignore b/vendor/gitignore/Maven.gitignore new file mode 100644 index 00000000000..1cdc9f7fd45 --- /dev/null +++ b/vendor/gitignore/Maven.gitignore @@ -0,0 +1,9 @@ +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties diff --git a/vendor/gitignore/Mercury.gitignore b/vendor/gitignore/Mercury.gitignore new file mode 100644 index 00000000000..70ec8693971 --- /dev/null +++ b/vendor/gitignore/Mercury.gitignore @@ -0,0 +1,13 @@ +Mercury/ +Mercury.modules +*.mh +*.err +*.init +*.dll +*.exe +*.a +*.so +*.dylib +*.beams +*.d +*.c_date diff --git a/vendor/gitignore/MetaProgrammingSystem.gitignore b/vendor/gitignore/MetaProgrammingSystem.gitignore new file mode 100644 index 00000000000..3e75841041c --- /dev/null +++ b/vendor/gitignore/MetaProgrammingSystem.gitignore @@ -0,0 +1,16 @@ +workspace.xml +junitvmwatcher*.properties +build.properties + +# generated java classes and java source files +# manually add any custom artifacts that can't be generated from the models +# http://confluence.jetbrains.com/display/MPSD25/HowTo+--+MPS+and+Git +classes_gen +source_gen +source_gen.caches + +# generated test code and test results +test_gen +test_gen.caches +TEST-*.xml +junit*.properties diff --git a/vendor/gitignore/Nanoc.gitignore b/vendor/gitignore/Nanoc.gitignore new file mode 100644 index 00000000000..abc21828a3e --- /dev/null +++ b/vendor/gitignore/Nanoc.gitignore @@ -0,0 +1,10 @@ +# For projects using nanoc (http://nanoc.ws/) + +# Default location for output, needs to match output_dir's value found in config.yaml +output/ + +# Temporary file directory +tmp/ + +# Crash Log +crash.log diff --git a/vendor/gitignore/Nim.gitignore b/vendor/gitignore/Nim.gitignore new file mode 100644 index 00000000000..67d9b34c6ce --- /dev/null +++ b/vendor/gitignore/Nim.gitignore @@ -0,0 +1 @@ +nimcache/ diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore new file mode 100644 index 00000000000..5148e527a7e --- /dev/null +++ b/vendor/gitignore/Node.gitignore @@ -0,0 +1,37 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history diff --git a/vendor/gitignore/OCaml.gitignore b/vendor/gitignore/OCaml.gitignore new file mode 100644 index 00000000000..f7817ae5c36 --- /dev/null +++ b/vendor/gitignore/OCaml.gitignore @@ -0,0 +1,20 @@ +*.annot +*.cmo +*.cma +*.cmi +*.a +*.o +*.cmx +*.cmxs +*.cmxa + +# ocamlbuild working directory +_build/ + +# ocamlbuild targets +*.byte +*.native + +# oasis generated files +setup.data +setup.log diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore new file mode 100644 index 00000000000..3020bc327a7 --- /dev/null +++ b/vendor/gitignore/Objective-C.gitignore @@ -0,0 +1,51 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xcuserstate + +## Obj-C/Swift specific +*.hmap +*.ipa + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md + +fastlane/report.xml +fastlane/screenshots diff --git a/vendor/gitignore/Opa.gitignore b/vendor/gitignore/Opa.gitignore new file mode 100644 index 00000000000..74c6219ceda --- /dev/null +++ b/vendor/gitignore/Opa.gitignore @@ -0,0 +1,13 @@ +_build +_tracks + +opa-debug-js + +*.opp +*.opx +*.opx.broken +*.dump +*.api +*.api-txt +*.exe +*.log diff --git a/vendor/gitignore/OpenCart.gitignore b/vendor/gitignore/OpenCart.gitignore new file mode 100644 index 00000000000..28e45aa6aac --- /dev/null +++ b/vendor/gitignore/OpenCart.gitignore @@ -0,0 +1,13 @@ +.htaccess +/config.php +admin/config.php + +!index.html + +download/ +image/data/ +image/cache/ +system/cache/ +system/logs/ + +system/storage/ diff --git a/vendor/gitignore/OracleForms.gitignore b/vendor/gitignore/OracleForms.gitignore new file mode 100644 index 00000000000..699a4940118 --- /dev/null +++ b/vendor/gitignore/OracleForms.gitignore @@ -0,0 +1,8 @@ +# Compiled Form Modules +*.fmx + +# Compiled Menu Modules +*.mmx + +# Compiled Pre-Linked Libraries +*.plx diff --git a/vendor/gitignore/Packer.gitignore b/vendor/gitignore/Packer.gitignore new file mode 100644 index 00000000000..1b7a03efdd7 --- /dev/null +++ b/vendor/gitignore/Packer.gitignore @@ -0,0 +1,5 @@ +# Cache objects +packer_cache/ + +# For built boxes +*.box diff --git a/vendor/gitignore/Perl.gitignore b/vendor/gitignore/Perl.gitignore new file mode 100644 index 00000000000..ae2ad536abb --- /dev/null +++ b/vendor/gitignore/Perl.gitignore @@ -0,0 +1,20 @@ +/blib/ +/.build/ +_build/ +cover_db/ +inc/ +Build +!Build/ +Build.bat +.last_cover_stats +/Makefile +/Makefile.old +/MANIFEST.bak +/META.yml +/META.json +/MYMETA.* +nytprof.out +/pm_to_blib +*.o +*.bs +/_eumm/ diff --git a/vendor/gitignore/Phalcon.gitignore b/vendor/gitignore/Phalcon.gitignore new file mode 100644 index 00000000000..6ffe3aa220a --- /dev/null +++ b/vendor/gitignore/Phalcon.gitignore @@ -0,0 +1,2 @@ +/cache/ +/config/development/ diff --git a/vendor/gitignore/PlayFramework.gitignore b/vendor/gitignore/PlayFramework.gitignore new file mode 100644 index 00000000000..6d67f119175 --- /dev/null +++ b/vendor/gitignore/PlayFramework.gitignore @@ -0,0 +1,15 @@ +# Ignore Play! working directory # +bin/ +/db +.eclipse +/lib/ +/logs/ +/modules +/project/target +/target +tmp/ +test-result +server.pid +*.eml +/dist/ +.cache diff --git a/vendor/gitignore/Plone.gitignore b/vendor/gitignore/Plone.gitignore new file mode 100644 index 00000000000..770a8681ac3 --- /dev/null +++ b/vendor/gitignore/Plone.gitignore @@ -0,0 +1,18 @@ +*.pyc +*.pyo +*.tmp* +*.mo +*.egg +*.EGG +*.egg-info +*.EGG-INFO +.*.cfg +bin/ +build/ +develop-eggs/ +downloads/ +eggs/ +fake-eggs/ +parts/ +dist/ +var/ diff --git a/vendor/gitignore/Prestashop.gitignore b/vendor/gitignore/Prestashop.gitignore new file mode 100644 index 00000000000..7c6ae1e31cc --- /dev/null +++ b/vendor/gitignore/Prestashop.gitignore @@ -0,0 +1,32 @@ +# Private files +# The following files contain your database credentials and other personal data. + +config/settings.*.php + +# Cache, temp and generated files +# The following files are generated by PrestaShop. + +admin-dev/autoupgrade/ +/cache/ +!/cache/index.php +!/cache/cachefs/index.php +!/cache/purifier/index.php +!/cache/push/index.php +!/cache/sandbox/index.php +!/cache/smarty/index.php +!/cache/tcpdf/index.php +config/xml/*.xml +/log/* +*sitemap.xml +themes/*/cache/ +modules/*/config*.xml + +# Site content +# The following folders contain product images, virtual products, CSV's, etc. + +admin-dev/backups/ +admin-dev/export/ +admin-dev/import/ +download/ +/img/* +upload/ diff --git a/vendor/gitignore/Processing.gitignore b/vendor/gitignore/Processing.gitignore new file mode 100644 index 00000000000..85f269a89f6 --- /dev/null +++ b/vendor/gitignore/Processing.gitignore @@ -0,0 +1,7 @@ +.DS_Store +applet +application.linux32 +application.linux64 +application.windows32 +application.windows64 +application.macosx diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore new file mode 100644 index 00000000000..72364f99fe4 --- /dev/null +++ b/vendor/gitignore/Python.gitignore @@ -0,0 +1,89 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject diff --git a/vendor/gitignore/Qooxdoo.gitignore b/vendor/gitignore/Qooxdoo.gitignore new file mode 100644 index 00000000000..d0c64102d85 --- /dev/null +++ b/vendor/gitignore/Qooxdoo.gitignore @@ -0,0 +1,5 @@ +cache +cache-downloads +inspector +api +source/inspector.html diff --git a/vendor/gitignore/Qt.gitignore b/vendor/gitignore/Qt.gitignore new file mode 100644 index 00000000000..fa24b2efee8 --- /dev/null +++ b/vendor/gitignore/Qt.gitignore @@ -0,0 +1,38 @@ +# C++ objects and libs + +*.slo +*.lo +*.o +*.a +*.la +*.lai +*.so +*.dll +*.dylib + +# Qt-es + +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +qrc_*.cpp +ui_*.h +Makefile* +*build-* + +# QtCreator + +*.autosave + +# QtCtreator Qml +*.qmlproject.user +*.qmlproject.user.* + +# QtCtreator CMake +CMakeLists.txt.user + diff --git a/vendor/gitignore/R.gitignore b/vendor/gitignore/R.gitignore new file mode 100644 index 00000000000..fcff087aebb --- /dev/null +++ b/vendor/gitignore/R.gitignore @@ -0,0 +1,33 @@ +# History files +.Rhistory +.Rapp.history + +# Session Data files +.RData + +# Example code in package build process +*-Ex.R + +# Output files from R CMD build +/*.tar.gz + +# Output files from R CMD check +/*.Rcheck/ + +# RStudio files +.Rproj.user/ + +# produced vignettes +vignettes/*.html +vignettes/*.pdf + +# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 +.httr-oauth + +# knitr and R markdown default cache directories +/*_cache/ +/cache/ + +# Temporary files created by R markdown +*.utf8.md +*.knit.md diff --git a/vendor/gitignore/README.md b/vendor/gitignore/README.md new file mode 100644 index 00000000000..43131e815cc --- /dev/null +++ b/vendor/gitignore/README.md @@ -0,0 +1,14 @@ +# .gitignore templates + +This directory contains language-specific .gitignore templates that are used by GitLab. + +These files were automatically pulled from [this repository](https://github.com/github/gitignore). +Please submit pull requests to that repository. There is no need to edit the files in this directory. + +## Bulk Update + +To update this directory with the latest changes in the repository, run: + +```sh +bundle exec rake gitlab:update_gitignore +``` diff --git a/vendor/gitignore/ROS.gitignore b/vendor/gitignore/ROS.gitignore new file mode 100644 index 00000000000..f8bcd117371 --- /dev/null +++ b/vendor/gitignore/ROS.gitignore @@ -0,0 +1,47 @@ +build/ +bin/ +lib/ +msg_gen/ +srv_gen/ +msg/*Action.msg +msg/*ActionFeedback.msg +msg/*ActionGoal.msg +msg/*ActionResult.msg +msg/*Feedback.msg +msg/*Goal.msg +msg/*Result.msg +msg/_*.py + +# Generated by dynamic reconfigure +*.cfgc +/cfg/cpp/ +/cfg/*.py + +# Ignore generated docs +*.dox +*.wikidoc + +# eclipse stuff +.project +.cproject + +# qcreator stuff +CMakeLists.txt.user + +srv/_*.py +*.pcd +*.pyc +qtcreator-* +*.user + +/planning/cfg +/planning/docs +/planning/src + +*~ + +# Emacs +.#* + +# Catkin custom files +CATKIN_IGNORE diff --git a/vendor/gitignore/Rails.gitignore b/vendor/gitignore/Rails.gitignore new file mode 100644 index 00000000000..2121e0a8038 --- /dev/null +++ b/vendor/gitignore/Rails.gitignore @@ -0,0 +1,38 @@ +*.rbc +capybara-*.html +.rspec +/log +/tmp +/db/*.sqlite3 +/db/*.sqlite3-journal +/public/system +/coverage/ +/spec/tmp +**.orig +rerun.txt +pickle-email-*.html + +# TODO Comment out these rules if you are OK with secrets being uploaded to the repo +config/initializers/secret_token.rb +config/secrets.yml + +## Environment normalization: +/.bundle +/vendor/bundle + +# these should all be checked in to normalize the environment: +# Gemfile.lock, .ruby-version, .ruby-gemset + +# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: +.rvmrc + +# if using bower-rails ignore default bower_components path bower.json files +/vendor/assets/bower_components +*.bowerrc +bower.json + +# Ignore pow environment settings +.powenv + +# Ignore Byebug command history file. +.byebug_history diff --git a/vendor/gitignore/RhodesRhomobile.gitignore b/vendor/gitignore/RhodesRhomobile.gitignore new file mode 100644 index 00000000000..a211dcc3b0f --- /dev/null +++ b/vendor/gitignore/RhodesRhomobile.gitignore @@ -0,0 +1,9 @@ +rholog-* +sim-* +bin/libs +bin/RhoBundle +bin/tmp +bin/target +bin/*.ap_ +*.o +*.jar diff --git a/vendor/gitignore/Ruby.gitignore b/vendor/gitignore/Ruby.gitignore new file mode 100644 index 00000000000..5e1422c9c3f --- /dev/null +++ b/vendor/gitignore/Ruby.gitignore @@ -0,0 +1,50 @@ +*.gem +*.rbc +/.config +/coverage/ +/InstalledFiles +/pkg/ +/spec/reports/ +/spec/examples.txt +/test/tmp/ +/test/version_tmp/ +/tmp/ + +# Used by dotenv library to load environment variables. +# .env + +## Specific to RubyMotion: +.dat* +.repl_history +build/ +*.bridgesupport +build-iPhoneOS/ +build-iPhoneSimulator/ + +## Specific to RubyMotion (use of CocoaPods): +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# vendor/Pods/ + +## Documentation cache and generated files: +/.yardoc/ +/_yardoc/ +/doc/ +/rdoc/ + +## Environment normalization: +/.bundle/ +/vendor/bundle +/lib/bundler/man/ + +# for a library or gem, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# Gemfile.lock +# .ruby-version +# .ruby-gemset + +# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: +.rvmrc diff --git a/vendor/gitignore/Rust.gitignore b/vendor/gitignore/Rust.gitignore new file mode 100644 index 00000000000..cb14a420640 --- /dev/null +++ b/vendor/gitignore/Rust.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock diff --git a/vendor/gitignore/SCons.gitignore b/vendor/gitignore/SCons.gitignore new file mode 100644 index 00000000000..39d9743a082 --- /dev/null +++ b/vendor/gitignore/SCons.gitignore @@ -0,0 +1,2 @@ +# for projects that use SCons for building: http://http://www.scons.org/ +.sconsign.dblite diff --git a/vendor/gitignore/Sass.gitignore b/vendor/gitignore/Sass.gitignore new file mode 100644 index 00000000000..486b32ce90c --- /dev/null +++ b/vendor/gitignore/Sass.gitignore @@ -0,0 +1,2 @@ +.sass-cache/ +*.css.map diff --git a/vendor/gitignore/Scala.gitignore b/vendor/gitignore/Scala.gitignore new file mode 100644 index 00000000000..c58d83b3189 --- /dev/null +++ b/vendor/gitignore/Scala.gitignore @@ -0,0 +1,17 @@ +*.class +*.log + +# sbt specific +.cache +.history +.lib/ +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ + +# Scala-IDE specific +.scala_dependencies +.worksheet diff --git a/vendor/gitignore/Scheme.gitignore b/vendor/gitignore/Scheme.gitignore new file mode 100644 index 00000000000..cbb89d78da5 --- /dev/null +++ b/vendor/gitignore/Scheme.gitignore @@ -0,0 +1,7 @@ +*.ss~ +*.ss#* +.#*.ss + +*.scm~ +*.scm#* +.#*.scm diff --git a/vendor/gitignore/Scrivener.gitignore b/vendor/gitignore/Scrivener.gitignore new file mode 100644 index 00000000000..3b39c66ba12 --- /dev/null +++ b/vendor/gitignore/Scrivener.gitignore @@ -0,0 +1,7 @@ +/Files/binder.autosave +/Files/binder.backup +/Files/search.indexes +/Files/user.lock +/Files/Docs/docs.checksum +/QuickLook/ +/Settings/ui.plist diff --git a/vendor/gitignore/Sdcc.gitignore b/vendor/gitignore/Sdcc.gitignore new file mode 100644 index 00000000000..07ee7d59aba --- /dev/null +++ b/vendor/gitignore/Sdcc.gitignore @@ -0,0 +1,8 @@ +# SDCC stuff +*.lnk +*.lst +*.map +*.mem +*.rel +*.rst +*.sym diff --git a/vendor/gitignore/SeamGen.gitignore b/vendor/gitignore/SeamGen.gitignore new file mode 100644 index 00000000000..a418cf376c5 --- /dev/null +++ b/vendor/gitignore/SeamGen.gitignore @@ -0,0 +1,26 @@ +/bootstrap/data +/bootstrap/tmp +/classes/ +/dist/ +/exploded-archives/ +/test-build/ +/test-output/ +/test-report/ +/target/ +temp-testng-customsuite.xml + +# based on http://stackoverflow.com/a/8865858/422476 I am removing inline comments + +#/classes/ all class files +#/dist/ contains generated war files for deployment +#/exploded-archives/ war content generation during deploy (or explode) +#/test-build/ test compilation (ant target for Seam) +#/test-output/ test results +#/test-report/ test report generation for, e.g., Hudson +#/target/ maven output folder +#temp-testng-customsuite.xml generated when running test cases under Eclipse + +# Thanks to @VonC and @kraftan for their helpful answers on a related question +# on StackOverflow.com: +# http://stackoverflow.com/questions/4176687 +# /what-is-the-recommended-source-control-ignore-pattern-for-seam-projects diff --git a/vendor/gitignore/SketchUp.gitignore b/vendor/gitignore/SketchUp.gitignore new file mode 100644 index 00000000000..5160df3c6bf --- /dev/null +++ b/vendor/gitignore/SketchUp.gitignore @@ -0,0 +1 @@ +*.skb diff --git a/vendor/gitignore/Smalltalk.gitignore b/vendor/gitignore/Smalltalk.gitignore new file mode 100644 index 00000000000..75272b23472 --- /dev/null +++ b/vendor/gitignore/Smalltalk.gitignore @@ -0,0 +1,18 @@ +# changes file +*.changes + +# system image +*.image + +# Pharo Smalltalk Debug log file +PharoDebug.log + +# Squeak Smalltalk Debug log file +SqueakDebug.log + +# Monticello package cache +/package-cache + +# Metacello-github cache +/github-cache +github-*.zip diff --git a/vendor/gitignore/Stella.gitignore b/vendor/gitignore/Stella.gitignore new file mode 100644 index 00000000000..402a5438373 --- /dev/null +++ b/vendor/gitignore/Stella.gitignore @@ -0,0 +1,12 @@ +# Atari 2600 (Stella) support for multiple assemblers +# - DASM +# - CC65 + +# Assembled binaries and object directories +obj/ +a.out +*.bin +*.a26 + +# Add in special Atari 7800-based binaries for good measure +*.a78 diff --git a/vendor/gitignore/SugarCRM.gitignore b/vendor/gitignore/SugarCRM.gitignore new file mode 100644 index 00000000000..842c3ec518b --- /dev/null +++ b/vendor/gitignore/SugarCRM.gitignore @@ -0,0 +1,25 @@ +## SugarCRM +# Ignore custom .htaccess stuff. +/.htaccess +# Ignore the cache directory completely. +# This will break the current behaviour. Which was often leading to +# the misuse of the repository as backup replacement. +# For development the cache directory can be safely ignored and +# therefore it is ignored. +/cache/ +# Ignore some files and directories from the custom directory. +/custom/history/ +/custom/modulebuilder/ +/custom/working/ +/custom/modules/*/Ext/ +/custom/application/Ext/ +# Custom configuration should also be ignored. +/config.php +/config_override.php +# The silent upgrade scripts aren't needed. +/silentUpgrade*.php +# Logs files can safely be ignored. +*.log +# Ignore the new upload directories. +/upload/ +/upload_backup/ diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore new file mode 100644 index 00000000000..8a29fa52af4 --- /dev/null +++ b/vendor/gitignore/Swift.gitignore @@ -0,0 +1,63 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +*.moved-aside +*.xcuserstate + +## Obj-C/Swift specific +*.hmap +*.ipa + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output diff --git a/vendor/gitignore/Symfony.gitignore b/vendor/gitignore/Symfony.gitignore new file mode 100644 index 00000000000..7d56f982f81 --- /dev/null +++ b/vendor/gitignore/Symfony.gitignore @@ -0,0 +1,48 @@ +# Cache and logs (Symfony2) +/app/cache/* +/app/logs/* +!app/cache/.gitkeep +!app/logs/.gitkeep + +# Email spool folder +/app/spool/* + +# Cache, session files and logs (Symfony3) +/var/cache/* +/var/logs/* +/var/sessions/* +!var/cache/.gitkeep +!var/logs/.gitkeep +!var/sessions/.gitkeep + +# Parameters +/app/config/parameters.yml +/app/config/parameters.ini + +# Managed by Composer +/app/bootstrap.php.cache +/var/bootstrap.php.cache +/bin/* +!bin/console +!bin/symfony_requirements +/vendor/ + +# Assets and user uploads +/web/bundles/ +/web/uploads/ + +# Assets managed by Bower +/web/assets/vendor/ + +# PHPUnit +/app/phpunit.xml +/phpunit.xml + +# Build data +/build/ + +# Composer PHAR +/composer.phar + +# Backup entities generated with doctrine:generate:entities command +*/Entity/*~ diff --git a/vendor/gitignore/SymphonyCMS.gitignore b/vendor/gitignore/SymphonyCMS.gitignore new file mode 100644 index 00000000000..671c7ff9e32 --- /dev/null +++ b/vendor/gitignore/SymphonyCMS.gitignore @@ -0,0 +1,6 @@ +manifest/cache/ +manifest/logs/ +manifest/tmp/ +symphony/ +workspace/uploads/ +install-log.txt diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore new file mode 100644 index 00000000000..4123a577c47 --- /dev/null +++ b/vendor/gitignore/TeX.gitignore @@ -0,0 +1,180 @@ +## Core latex/pdflatex auxiliary files: +*.aux +*.lof +*.log +*.lot +*.fls +*.out +*.toc +*.fmt +*.fot +*.cb +*.cb2 + +## Intermediate documents: +*.dvi +*-converted-to.* +# these rules might exclude image files for figures etc. +# *.ps +# *.eps +# *.pdf + +## Bibliography auxiliary files (bibtex/biblatex/biber): +*.bbl +*.bcf +*.blg +*-blx.aux +*-blx.bib +*.brf +*.run.xml + +## Build tool auxiliary files: +*.fdb_latexmk +*.synctex +*.synctex.gz +*.synctex.gz(busy) +*.pdfsync + +## Auxiliary and intermediate files from other packages: +# algorithms +*.alg +*.loa + +# achemso +acs-*.bib + +# amsthm +*.thm + +# beamer +*.nav +*.snm +*.vrb + +# cprotect +*.cpt + +# fixme +*.lox + +#(r)(e)ledmac/(r)(e)ledpar +*.end +*.?end +*.[1-9] +*.[1-9][0-9] +*.[1-9][0-9][0-9] +*.[1-9]R +*.[1-9][0-9]R +*.[1-9][0-9][0-9]R +*.eledsec[1-9] +*.eledsec[1-9]R +*.eledsec[1-9][0-9] +*.eledsec[1-9][0-9]R +*.eledsec[1-9][0-9][0-9] +*.eledsec[1-9][0-9][0-9]R + +# glossaries +*.acn +*.acr +*.glg +*.glo +*.gls +*.glsdefs + +# gnuplottex +*-gnuplottex-* + +# hyperref +*.brf + +# knitr +*-concordance.tex +# TODO Comment the next line if you want to keep your tikz graphics files +*.tikz +*-tikzDictionary + +# listings +*.lol + +# makeidx +*.idx +*.ilg +*.ind +*.ist + +# minitoc +*.maf +*.mlf +*.mlt +*.mtc +*.mtc[0-9] +*.mtc[1-9][0-9] + +# minted +_minted* +*.pyg + +# morewrites +*.mw + +# mylatexformat +*.fmt + +# nomencl +*.nlo + +# sagetex +*.sagetex.sage +*.sagetex.py +*.sagetex.scmd + +# sympy +*.sout +*.sympy +sympy-plots-for-*.tex/ + +# pdfcomment +*.upa +*.upb + +# pythontex +*.pytxcode +pythontex-files-*/ + +# thmtools +*.loe + +# TikZ & PGF +*.dpth +*.md5 +*.auxlock + +# todonotes +*.tdo + +# xindy +*.xdy + +# xypic precompiled matrices +*.xyc + +# endfloat +*.ttt +*.fff + +# Latexian +TSWLatexianTemp* + +## Editors: +# WinEdt +*.bak +*.sav + +# Texpad +.texpadtmp + +# Kile +*.backup + +# KBibTeX +*~[0-9]* diff --git a/vendor/gitignore/Terraform.gitignore b/vendor/gitignore/Terraform.gitignore new file mode 100644 index 00000000000..7868d16d216 --- /dev/null +++ b/vendor/gitignore/Terraform.gitignore @@ -0,0 +1,3 @@ +# Compiled files +*.tfstate +*.tfstate.backup diff --git a/vendor/gitignore/Textpattern.gitignore b/vendor/gitignore/Textpattern.gitignore new file mode 100644 index 00000000000..3805636d622 --- /dev/null +++ b/vendor/gitignore/Textpattern.gitignore @@ -0,0 +1,11 @@ +.htaccess +css.php +rpc/ +sites/site*/admin/ +sites/site*/private/ +sites/site*/public/admin/ +sites/site*/public/setup/ +sites/site*/public/theme/ +textpattern/ +HISTORY.txt +README.txt diff --git a/vendor/gitignore/TurboGears2.gitignore b/vendor/gitignore/TurboGears2.gitignore new file mode 100644 index 00000000000..122b3de221f --- /dev/null +++ b/vendor/gitignore/TurboGears2.gitignore @@ -0,0 +1,20 @@ +*.py[co] + +# Default development database +devdata.db + +# Default data directory +data/* + +# Packages +*.egg +*.egg-info +dist +build + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox diff --git a/vendor/gitignore/Typo3.gitignore b/vendor/gitignore/Typo3.gitignore new file mode 100644 index 00000000000..cb024fefe99 --- /dev/null +++ b/vendor/gitignore/Typo3.gitignore @@ -0,0 +1,20 @@ +## TYPO3 v6.2 +# Ignore several upload and file directories. +/fileadmin/user_upload/ +/fileadmin/_temp_/ +/fileadmin/_processed_/ +/uploads/ +# Ignore cache +/typo3conf/temp_CACHED* +/typo3conf/temp_fieldInfo.php +/typo3conf/deprecation_*.log +/typo3conf/AdditionalConfiguration.php +# Ignore system folders, you should have them symlinked. +# If not comment out the following entries. +/typo3 +/typo3_src +/typo3_src-* +/.htaccess +/index.php +# Ignore temp directory. +/typo3temp/ diff --git a/vendor/gitignore/Umbraco.gitignore b/vendor/gitignore/Umbraco.gitignore new file mode 100644 index 00000000000..ea05e1fb2a9 --- /dev/null +++ b/vendor/gitignore/Umbraco.gitignore @@ -0,0 +1,19 @@ +# Note: VisualStudio gitignore rules may also be relevant + +# Umbraco +# Ignore unimportant folders generated by Umbraco +**/App_Data/Logs/ +**/App_Data/[Pp]review/ +**/App_Data/TEMP/ +**/App_Data/NuGetBackup/ + +# Ignore Umbraco content cache file +**/App_Data/umbraco.config + +# Don't ignore Umbraco packages (VisualStudio.gitignore mistakes this for a NuGet packages folder) +# Make sure to include details from VisualStudio.gitignore BEFORE this +!**/App_Data/[Pp]ackages/ +!**/[Uu]mbraco/[Dd]eveloper/[Pp]ackages + +# ImageProcessor DiskCache +**/App_Data/cache/ diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore new file mode 100644 index 00000000000..5aafcbb7f1d --- /dev/null +++ b/vendor/gitignore/Unity.gitignore @@ -0,0 +1,30 @@ +/[Ll]ibrary/ +/[Tt]emp/ +/[Oo]bj/ +/[Bb]uild/ +/[Bb]uilds/ +/Assets/AssetStoreTools* + +# Autogenerated VS/MD solution and project files +ExportedObj/ +*.csproj +*.unityproj +*.sln +*.suo +*.tmp +*.user +*.userprefs +*.pidb +*.booproj +*.svd + + +# Unity3D generated meta files +*.pidb.meta + +# Unity3D Generated File On Crash Reports +sysinfo.txt + +# Builds +*.apk +*.unitypackage diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore new file mode 100644 index 00000000000..75b1186b0af --- /dev/null +++ b/vendor/gitignore/UnrealEngine.gitignore @@ -0,0 +1,62 @@ +# Visual Studio 2015 user specific files +.vs/ + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app +*.ipa + +# These project files can be generated by the engine +*.xcodeproj +*.sln +*.suo +*.opensdf +*.sdf +*.VC.opendb + +# Precompiled Assets +SourceArt/**/*.png +SourceArt/**/*.tga + +# Binary Files +Binaries/* + +# Builds +Build/* + +# Don't ignore icon files in Build +!Build/**/*.ico + +# Configuration files generated by the Editor +Saved/* + +# Compiled source files for the engine to use +Intermediate/* + +# Cache files for the editor to use +DerivedDataCache/* diff --git a/vendor/gitignore/VVVV.gitignore b/vendor/gitignore/VVVV.gitignore new file mode 100644 index 00000000000..5df4324603e --- /dev/null +++ b/vendor/gitignore/VVVV.gitignore @@ -0,0 +1,6 @@ + +# .v4p backup files +*~.xml + +# Dynamic plugins .dll +bin/ diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore new file mode 100644 index 00000000000..f1e3d20e056 --- /dev/null +++ b/vendor/gitignore/VisualStudio.gitignore @@ -0,0 +1,252 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml diff --git a/vendor/gitignore/Waf.gitignore b/vendor/gitignore/Waf.gitignore new file mode 100644 index 00000000000..48e8d8f7be4 --- /dev/null +++ b/vendor/gitignore/Waf.gitignore @@ -0,0 +1,4 @@ +# for projects that use Waf for building: http://code.google.com/p/waf/ +.waf-* +.waf3-* +.lock-* diff --git a/vendor/gitignore/WordPress.gitignore b/vendor/gitignore/WordPress.gitignore new file mode 100644 index 00000000000..97923503c4c --- /dev/null +++ b/vendor/gitignore/WordPress.gitignore @@ -0,0 +1,18 @@ +*.log +wp-config.php +wp-content/advanced-cache.php +wp-content/backup-db/ +wp-content/backups/ +wp-content/blogs.dir/ +wp-content/cache/ +wp-content/upgrade/ +wp-content/uploads/ +wp-content/wp-cache-config.php +wp-content/plugins/hello.php + +/.htaccess +/license.txt +/readme.html +/sitemap.xml +/sitemap.xml.gz + diff --git a/vendor/gitignore/Xojo.gitignore b/vendor/gitignore/Xojo.gitignore new file mode 100644 index 00000000000..1b036dd4f2e --- /dev/null +++ b/vendor/gitignore/Xojo.gitignore @@ -0,0 +1,11 @@ +# Xojo (formerly REALbasic and Real Studio) + +Builds* +*.debug +*.debug.app +Debug*.exe +Debug*/Debug*.exe +Debug*/Debug*\ Libs +*.rbuistate +*.xojo_uistate +*.obsolete diff --git a/vendor/gitignore/Yeoman.gitignore b/vendor/gitignore/Yeoman.gitignore new file mode 100644 index 00000000000..7170d72018d --- /dev/null +++ b/vendor/gitignore/Yeoman.gitignore @@ -0,0 +1,6 @@ +node_modules/ +bower_components/ +*.log + +build/ +dist/ diff --git a/vendor/gitignore/Yii.gitignore b/vendor/gitignore/Yii.gitignore new file mode 100644 index 00000000000..70f087546f2 --- /dev/null +++ b/vendor/gitignore/Yii.gitignore @@ -0,0 +1,6 @@ +assets/* +!assets/.gitignore +protected/runtime/* +!protected/runtime/.gitignore +protected/data/*.db +themes/classic/views/ diff --git a/vendor/gitignore/ZendFramework.gitignore b/vendor/gitignore/ZendFramework.gitignore new file mode 100644 index 00000000000..80adb154900 --- /dev/null +++ b/vendor/gitignore/ZendFramework.gitignore @@ -0,0 +1,25 @@ +# Composer files +composer.phar +vendor/ + +# Local configs +config/autoload/*.local.php + +# Binary gettext files +*.mo + +# Data +data/logs/ +data/cache/ +data/sessions/ +data/tmp/ +temp/ + +#Doctrine 2 +data/DoctrineORMModule/Proxy/ +data/DoctrineORMModule/cache/ + + +# Legacy ZF1 +demos/ +extras/documentation diff --git a/vendor/gitignore/Zephir.gitignore b/vendor/gitignore/Zephir.gitignore new file mode 100644 index 00000000000..839cb5d7070 --- /dev/null +++ b/vendor/gitignore/Zephir.gitignore @@ -0,0 +1,26 @@ +# Cache files, generates by Zephir +.temp/ +.libs/ + +# Object files, generates by linker +*.lo +*.la +*.o +*.loT + +# Files generated by configure and Zephir, +# not required for extension compilation. +ext/build/ +ext/modules/ +ext/Makefile* +ext/config* +ext/acinclude.m4 +ext/aclocal.m4 +ext/autom4te* +ext/install-sh +ext/ltmain.sh +ext/missing +ext/mkinstalldirs +ext/run-tests.php +ext/.deps +ext/libtool |